Welcome, guest | Sign In | My Account | Store | Cart

Wether or not PEP 318 makes it to Python 2.4, you can experiment with an alternative decorator syntax in Python 2.3 by hacking with bytecodes.

Python, 107 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
import dis, new

def _pop_funclist(f):
    """_pop_funclist(f) -> list or None

    Evaluates and returns a list constant defined at the beginning
    of a function. If the function doesn't begin with a list,
    or the list refers to parameters or other locals, a None is returned.
    The returned list is removed from the function code.
    """
    op = dis.opmap.__getitem__
    i = 0
    co = f.func_code
    s = co.co_code
    stopcodes = [op('LOAD_FAST'), op('STORE_FAST'), op('STORE_NAME'),
                  op('POP_TOP'), op('JUMP_FORWARD')]
    while i < len(s):
        code = ord(s[i])
        i += 1
        if code >= dis.HAVE_ARGUMENT:
            i += 2
        if code in stopcodes:
            return
        if code == op('BUILD_LIST') and ord(s[i]) == op('POP_TOP'):
            i += 1
            break
    else:
        return
    varname = '__func_list__'
    names = co.co_names + (varname,)
    dict_code = co.co_code[:i-1] + ''.join(map(chr, [
        op('STORE_NAME'),
        list(names).index(varname), 0,        
        op('LOAD_CONST'),
        list(co.co_consts).index(None), 0,
        op('RETURN_VALUE'),
        ]))
    func_code = chr(op('JUMP_FORWARD')) \
                + chr(i-3) + chr(0) \
                + co.co_code[3:]
    list_co = new.code(0, 0, co.co_stacksize, 64, dict_code,
                       co.co_consts, names, co.co_varnames, co.co_filename,
                       co.co_name, co.co_firstlineno, co.co_lnotab)
    func_co = new.code(co.co_argcount, co.co_nlocals, co.co_stacksize, co.co_flags, func_code,
                       co.co_consts, co.co_names, co.co_varnames, co.co_filename,
                       co.co_name, co.co_firstlineno, co.co_lnotab)
    f.func_code = func_co
    globals = f.func_globals.copy()
    exec list_co in globals
    result = globals[varname]
    return result
    
def decorate(func):
    """decorate(func) -> func

    Gets the decorator list from a function and if found,
    applies the decorators in order and returns the transformed
    function.
    """    
    funclist = _pop_funclist(func)
    if funclist is None: return func
    f = func
    for d in funclist:
        if callable(d):
            f = d(f)
    return f
    

def attrs(**kw):
    def setattrs(f):
        f.__dict__.update(kw)
        return f
    return setattrs

class list_decorators(type):
    """
    Metaclass that calls the decorate function for all
    methods defined in the class.
    """
    def __init__(cls, name, bases, dct):
        type.__init__(cls, name, bases, dct)
        for k,v in dct.iteritems():
            if hasattr(v, "func_code"):
                setattr(cls, k, decorate(v))
    
class TestClass(object):
    __metaclass__ = list_decorators

    def normal(self):
        self.x = 10

    def static(x):
        [attrs(author="shang", version=2),
         staticmethod]
        print x

    def name(cls):
        [classmethod]
        return cls.__name__


>>> TestClass.static.author
'shang'
>>> TestClass.static.version
2
>>> TestClass.name()
'TestClass'

The _pop_funclist function looks for the list building bytecodes in the beginning of the function and executes them to generate the list. It then "removes" them from the function by adding a FORWARD_JUMP bytecode in the beginning of the function that skips the list initialization (this is done so that you can use slow functions and functions with side-effects in the decorator list).

2 comments

Bob Ippolito 19 years, 10 months ago  # | flag

This is interesting, but wasn't Guido's proposal to put it immediately before function definition, not inside the function body?

Sami Hangaslammi (author) 19 years, 10 months ago  # | flag

True, but it was a bit cleaner to implement it the other way around, since this way the list ends up with the function object when compiled. I'll see if I can implement Guido's actual proposal by going through the module level bytecodes.

Created by Sami Hangaslammi on Fri, 18 Jun 2004 (PSF)
Python recipes (4591)
Sami Hangaslammi's recipes (7)

Required Modules

  • (none specified)

Other Information and Tasks