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.
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).
This is interesting, but wasn't Guido's proposal to put it immediately before function definition, not inside the function body?
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.