Code evaluation at function definition execution time (was Re: Compile time evaluation (aka eliminating default argument hacks))
Bengt Richter
bokr at oz.net
Thu Mar 10 13:08:38 EST 2005
On Fri, 25 Feb 2005 19:34:53 -0700, Steven Bethard <steven.bethard at gmail.com> wrote:
>Nick Coghlan wrote:
>> Anyway, if others agree that the ability to execute a suite at def
>> exeuction time to preinitialise a function's locals without resorting to
>> bytecode hacks is worth having, finding a decent syntax is the next
Well, what if the bytecode hacks were part of a builtin decorator? Of course,
someone would have to make sure versions were updated, but that's true of
such things as the dis module too.
>> trick :)
>
>I'm not certain how many use cases really require a full suite, though
>being able to define default values for function locals in the same way
>that default values can be defined for function arguments would be nice.
>
Enjoy ;-)
>Worth looking at is the thread:
>
>http://groups-beta.google.com/group/comp.lang.python/browse_thread/thread/58f53fe8bcc49664/
>
Before I went away I guess I sort of promised to post "my" (obviously, credits/thanks to Raymond) hack,
so here it is. BTW it also tries to modify the line map and update signature for currying,
but it is _minimally_ tested:
(Note that this way of currying does not result in nested calls and closures, so currying should
result in speedup rather than slowdown, even without applying Raymond's optimizing decorator ;-)
----< presets.py >-------------------------------------------------------------------------
# presets.py -- a decorator to preset function local variables without a default-argument hack or closure
# also does currying, with adjustment of argument count, eliminating named arguments from right.
# 20050310 09:22:15 -- alpha 0.01 release -- bokr
# Released to the public domain WITH NO WARRANTIES of any kind by Bengt Richter
# Released to c.l.py for testing and/or further development by the interested.
# Byte code munging based on cannibalizing Raymond Hettinger's make_constants optimizing decorator (presets.py
# doesn't do the optimizations, though make_constants should be able to process the output of presets if applied
# outermost).
#
if __import__('sys').version_info[:2] != (2, 4):
raise SystemExit, 'presets.py requires version 2.4 at least, and maybe exactly.'
from opcode import opmap, HAVE_ARGUMENT, EXTENDED_ARG, hasjabs
globals().update(opmap)
class ShouldNotHappenError(Exception): pass
def presets(verbose=False, **presets):
"""
Print preset change info if verbose.
All keyword values are injected into the decorated function's
local namespace as intial assignments to local variables.
A function may make use of the variables before apparently setting them.
Global references will be overridden and made into local preset variable
references if they are present as keyword arguments.
"""
return lambda f: _presets(f, False, verbose, **presets)
def curry(verbose=False, **curry):
"""
return a function with named arguments replaced with given expression values
and eliminated from signature. Multiple arguments may be eliminated but names
must be taken from the right of the signature without skipping.
"""
return lambda f: _curry(f, verbose, **curry)
def _curry(f, verbose, **curry):
try:
co = f.func_code
except AttributeError:
return f # Jython doesn't have a func_code attribute.
if not curry: return f # nothing to do
names = co.co_names
varnames = list(co.co_varnames)[:co.co_argcount] # for indexing local names
if len(curry) > len(varnames):
raise ValueError, 'too many curry values %r vs %r'%(curry.keys(), varnames)
for n, name in enumerate(varnames[::-1]):
if n >= len(curry): break
if name not in curry:
raise ValueError, 'must supply %r before others in arg list %r'%(name, varnames)
return _presets(f, True, verbose, **curry)
def _presets(f, curry=False, verbose=False, **presets):
try:
co = f.func_code
except AttributeError:
return f # Jython doesn't have a func_code attribute.
if not presets: return f # nothing to do
newcode = map(ord, co.co_code)
newconsts = list(co.co_consts)
names = co.co_names
codelen = len(newcode)
varnames = list(co.co_varnames) # for indexing local names
nvarnames = len(varnames) # for later check if any added
prenames = tuple(sorted(presets))
nseq = len(prenames)
pretuple = tuple(presets[name] for name in prenames)
pos = len(newconsts)
newconsts.append(nseq > 1 and pretuple or pretuple[0])
if verbose: print '\npresets: -- "name(?)" means name may be unused'
# generate the code to set presets (by unpacking the constant tuple of values if more than one value)
precode = [LOAD_CONST, pos&0xFF, pos >> 8] # single value or tuple to unpack
if nseq > 1: precode.extend([ UNPACK_SEQUENCE, nseq&0xff, nseq>>8])
for name in prenames:
try: ix = varnames.index(name) # look for local name
except ValueError:
ix = len(varnames)
varnames.append(name) # make sure there is a local variable as such for the preset name
if verbose:
print '%12s%s = %r' % (name, '(?)'*(name not in names), presets[name])
precode.extend([STORE_FAST, ix&0xff, ix>>8])
if verbose: print
precodelen = len(precode)
# Change preset-name global references to local names and references
# adjust absolute jumps for length of presetting code
i = 0
posdict = {}
while i < codelen:
opcode = newcode[i]
if opcode in (EXTENDED_ARG, STORE_GLOBAL):
return f # XXX ?? for simplicity, only preset for common cases ??
if opcode in (LOAD_GLOBAL, LOAD_NAME, LOAD_CLOSURE, LOAD_DEREF):
oparg = newcode[i+1] + (newcode[i+2] << 8)
if opcode in (LOAD_GLOBAL, LOAD_NAME):
name = co.co_names[oparg]
else:
name = (co.co_cellvars + co.co_freevars)[oparg]
if name in presets:
# change code to LOAD_FAST of new local
try: ix = varnames.index(name)
except ValueError:
raise ShouldNotHappenError, "--> can't find new local %r in %r" % (name, varnames)
else:
newcode[i] = LOAD_FAST
newcode[i+1] = ix&0xff
newcode[i+2] = ix>>8
elif precodelen and opcode in hasjabs: # JUMP_ABSOLUTE or CONTINUE_LOOP
jdest = newcode[i+1] + (newcode[i+2]<<8)
jdest += precodelen # abs positions have moved down by presetting-code's length
newcode[i+1] = jdest&0xff
newcode[i+2] = jdest>>8
i += 1
if opcode >= HAVE_ARGUMENT:
i += 2
newlocals = len(varnames)-nvarnames
codestr = ''.join(map(chr, precode+newcode))
argcount = co.co_argcount
defaults = f.func_defaults
if curry:
argcount -= len(presets)
defaults = defaults and defaults[:len(presets)]
codeobj = type(co)(argcount, co.co_nlocals+newlocals, co.co_stacksize,
co.co_flags, codestr, tuple(newconsts), co.co_names,
tuple(varnames), co.co_filename, co.co_name,
co.co_firstlineno, '%c%c%c%c%s'%(0, 0, precodelen, 2, co.co_lnotab[2:]), co.co_freevars,
co.co_cellvars)
return type(f)(codeobj, f.func_globals, f.func_name, defaults, f.func_closure)
def test():
@presets(
verbose=True,
decotime=__import__('time').ctime(),
ver = __import__('sys').version,
test_unused = 'verbose output should show postfixed (?) on this variable',
comment = 'XXX the presets decorator needs much more testing!'
)
def foo():
print
print 'Note: foo was decorated on %s' % decotime
print 'Python version %s' % ver
print
print comment
print
foo()
print
print 'Curried def bar(x, y):return x*y with y=111, printing bar(2), bar(3):'
@curry(y=111)
def bar(x, y): return x*y
print bar(2), bar(3)
return foo, bar
if __name__ == '__main__':
test()
-------------------------------------------------------------------------------------------
Results:
[ 9:47] C:\pywk\clp>py24 ..\ut\presets.py
presets: -- "name(?)" means name may be unused
comment = 'XXX the presets decorator needs much more testing!'
decotime = 'Thu Mar 10 09:47:27 2005'
test_unused(?) = 'verbose output should show postfixed (?) on this variable'
ver = '2.4b1 (#56, Nov 3 2004, 01:47:27) \n[GCC 3.2.3 (mingw special 20030504-1)]'
Note: foo was decorated on Thu Mar 10 09:47:27 2005
Python version 2.4b1 (#56, Nov 3 2004, 01:47:27)
[GCC 3.2.3 (mingw special 20030504-1)]
XXX the presets decorator needs much more testing!
Curried def bar(x, y):return x*y with y=111, printing bar(2), bar(3):
222 333
Regards,
Bengt Richter
More information about the Python-list
mailing list