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