[Python-ideas] Proposal for new-style decorators
Mathias Panzenböck
grosser.meister.morti at gmx.net
Fri Apr 29 18:37:46 CEST 2011
I thought about this problem for I wile and I came up with this @decorator:
Usage:
------
>>> @decorator
>>> def my_deco(func,func_args,func_kwargs,deco_args...):
>>> pass
>>>
>>> @my_deco(*deco_args,**deco_kwargs)
>>> def func(*func_args,**func_kwargs):
>>> pass
Or a more specific example:
>>> @decorator
>>> def deco(func,func_args,func_kwargs,a,b=12,**kwargs):
>>> return (func(*func_args,**func_kwargs),func_args,func_kwargs,a,b,kwargs)
>>>
>>> @deco(1,f=12)
>>> def foo(x,y,z=2,*args):
>>> return (x,y,z,args)
>>>
>>> foo(5,6)
((5, 6, 2, ()), (5, 6, 2), {}, 1, 12, {'f': 12})
This fully supports *args and **kwargs besides regular arguments for the decorator and the decorated
function. By that I mean func_args already contains the filled in default values of func as well as
any regular arguments that where passed as keyword argument and argument passing errors are handled
(e.g. passing an argument as positional and keyword argument).
Error handling example:
>>> @deco(1,2,3)
>>> def bar(x,y,z=2,*args):
>>> return (x,y,z,args)
>>>
Traceback (most recent call last):
File "<pyshell#114>", line 1, in <module>
@deco(1,2,3)
File "<pyshell#96>", line 22, in _deco_deco
deco_args, deco_kwargs = apply_deco_args(*deco_args, **deco_kwargs)
TypeError: deco() takes at most 2 arguments (3 given)
Or:
>>> foo(5,6,y=33)
Traceback (most recent call last):
File "<pyshell#112>", line 1, in <module>
foo(5,6,y=33)
File "<pyshell#96>", line 27, in _f
func_args, func_kwargs = apply_func_args(*func_args, **func_kwargs)
TypeError: foo() got multiple values for keyword argument 'y'
Of course that always needs function call parenthesis on the decorator, even if the decorator does
not take any arguments. Maybe it could be extended that in this case a more simple decorator
mechanism is used. A decorator-decorator for decorators without arguments would be very simple (see
end of mail).
Implementation:
---------------
from types import FunctionType, ClassType
from functools import wraps
def inspect_callable(func):
"""-> (arg_names, co_flags, func_defaults, func_name)"""
return _inspect_callable(func,set())
def _inspect_callable(func,visited):
if func in visited:
raise TypeError("'%s' object is not callable" % type(func).__name__)
visited.add(func)
if isinstance(func, FunctionType):
co = func.func_code
func_name = func.__name__
arg_names = list(co.co_varnames[0:co.co_argcount])
defaults = func.func_defaults
flags = co.co_flags
elif isinstance(func, ClassType):
func_name = func.__name__
arg_names, flags, defaults, member_name = _inspect_callable(func.__init__,visited)
if arg_names:
del arg_names[0]
elif hasattr(func, '__call__'):
func_name = '<%s object at 0x%x>' % (type(func).__name__, id(func))
arg_names, flags, defaults, member_name = _inspect_callable(func.__call__,visited)
else:
raise TypeError("'%s' object is not callable" % type(func).__name__)
return arg_names, flags, defaults, func_name
FUNC_ARGS = 0x04
FUNC_KWARGS = 0x08
FUNC_GEN = 0x20
# this function should probably be reimplemented in C:
def args_applyer(arg_names,flags=0,defaults=None,func_name=None):
"""-> f(args..., [*varargs], [**kwargs]) -> ((args...)+varargs, kwargs)"""
all_args = list(arg_names)
if arg_names:
body = ['(',','.join(arg_names),')']
else:
body = []
if flags & FUNC_ARGS:
args_name = '_args'
i = 0
while args_name in arg_names:
args_name = '_args'+i
i += 1
all_args.append('*'+args_name)
if arg_names:
body.append('+')
body.append(args_name)
elif not arg_names:
body.append('()')
body.append(',')
if flags & FUNC_KWARGS:
kwargs_name = '_kwargs'
i = 0
while kwargs_name in arg_names:
kwargs_name = '_kwargs'+i
i += 1
all_args.append('**'+kwargs_name)
body.append(kwargs_name)
else:
body.append('{}')
if func_name:
apply_args = named_lambda(func_name,all_args,''.join(body))
else:
apply_args = eval('lambda %s: (%s)' % (','.join(all_args), ''.join(body)))
if defaults:
apply_args.func_defaults = defaults
return apply_args
def named_lambda(name,args,body):
code = 'def _named_lambda():\n\tdef %s(%s):\n\t\treturn %s\n\treturn %s' % (
name, ','.join(args), body, name)
del name, args, body
exec(code)
return _named_lambda()
# begin helper functions (not used by this module but might be handy for decorator developers)
def args_applyer_for(func):
return args_applyer(*inspect_callable(func))
def apply_args(args,kwargs,arg_names,flags=0,defaults=None,func_name=None):
return args_applyer(arg_names,flags,defaults,func_name)(*args,**kwargs)
def apply_args_for(func,args,kwargs):
return args_applyer(*inspect_callable(func))(*args,**kwargs)
# end helper functions
def decorator(deco):
"""deco(func,func_args,func_kwargs,deco_args...)
@decorator
def my_deco(func,func_args,func_kwargs,deco_args...):
pass
@my_deco(*deco_args,**deco_kwargs)
def func(*func_args,**func_kwargs):
pass
"""
arg_names, flags, defaults, deco_name = inspect_callable(deco)
if flags & FUNC_ARGS == 0:
if len(arg_names) < 3:
raise TypeError('decorator functions need at least 3 ' +
'arguments (func, func_args, func_kwargs)')
del arg_names[0:3]
apply_deco_args = args_applyer(arg_names,flags,defaults,deco_name)
del flags, defaults
@wraps(deco)
def _deco_deco(*deco_args,**deco_kwargs):
deco_args, deco_kwargs = apply_deco_args(*deco_args, **deco_kwargs)
def _deco(func):
apply_func_args = args_applyer(*inspect_callable(func))
@wraps(func)
def _f(*func_args,**func_kwargs):
func_args, func_kwargs = apply_func_args(*func_args, **func_kwargs)
return deco(func,func_args,func_kwargs,*deco_args,**deco_kwargs)
return _f
return _deco
return _deco_deco
def simple_decorator(deco):
"""deco(func,func_args,func_kwargs)
@simple_decorator
def my_deco(func,func_args,func_kwargs):
pass
@my_deco
def func(*func_args,**func_kwargs):
pass
"""
@wraps(deco)
def _deco(func):
apply_func_args = args_applyer(*inspect_callable(func))
@wraps(func)
def _f(*args,**kwargs):
return deco(func,*apply_func_args(*args,**kwargs))
return _f
return _deco
More information about the Python-ideas
mailing list