Decorators with parameters?
The syntax for decorators with arguments is a bit different - the decorator with arguments should return a function that will take a function and return another function. So it should really return a normal decorator. A bit confusing, right? What I mean is:
def decorator_factory(argument): def decorator(function): def wrapper(*args, **kwargs): funny_stuff() something_with_argument(argument) result = function(*args, **kwargs) more_funny_stuff() return result return wrapper return decorator
Here you can read more on the subject - it's also possible to implement this using callable objects and that is also explained there.
Edit : for an in-depth understanding of the mental model of decorators, take a look at this awesome Pycon Talk. well worth the 30 minutes.
One way of thinking about decorators with arguments is
@decoratordef foo(*args, **kwargs): pass
translates to
foo = decorator(foo)
So if the decorator had arguments,
@decorator_with_args(arg)def foo(*args, **kwargs): pass
translates to
foo = decorator_with_args(arg)(foo)
decorator_with_args
is a function which accepts a custom argument and which returns the actual decorator (that will be applied to the decorated function).
I use a simple trick with partials to make my decorators easy
from functools import partialdef _pseudo_decor(fun, argument): def ret_fun(*args, **kwargs): #do stuff here, for eg. print ("decorator arg is %s" % str(argument)) return fun(*args, **kwargs) return ret_funreal_decorator = partial(_pseudo_decor, argument=arg)@real_decoratordef foo(*args, **kwargs): pass
Update:
Above, foo
becomes real_decorator(foo)
One effect of decorating a function is that the name foo
is overridden upon decorator declaration. foo
is "overridden" by whatever is returned by real_decorator
. In this case, a new function object.
All of foo
's metadata is overridden, notably docstring and function name.
>>> print(foo)<function _pseudo_decor.<locals>.ret_fun at 0x10666a2f0>
functools.wraps gives us a convenient method to "lift" the docstring and name to the returned function.
from functools import partial, wrapsdef _pseudo_decor(fun, argument): # magic sauce to lift the name and doc of the function @wraps(fun) def ret_fun(*args, **kwargs): # pre function execution stuff here, for eg. print("decorator argument is %s" % str(argument)) returned_value = fun(*args, **kwargs) # post execution stuff here, for eg. print("returned value is %s" % returned_value) return returned_value return ret_funreal_decorator1 = partial(_pseudo_decor, argument="some_arg")real_decorator2 = partial(_pseudo_decor, argument="some_other_arg")@real_decorator1def bar(*args, **kwargs): pass>>> print(bar)<function __main__.bar(*args, **kwargs)>>>> bar(1,2,3, k="v", x="z")decorator argument is some_argreturned value is None
I'd like to show an idea which is IMHO quite elegant. The solution proposed by t.dubrownik shows a pattern which is always the same: you need the three-layered wrapper regardless of what the decorator does.
So I thought this is a job for a meta-decorator, that is, a decorator for decorators. As a decorator is a function, it actually works as a regular decorator with arguments:
def parametrized(dec): def layer(*args, **kwargs): def repl(f): return dec(f, *args, **kwargs) return repl return layer
This can be applied to a regular decorator in order to add parameters. So for instance, say we have the decorator which doubles the result of a function:
def double(f): def aux(*xs, **kws): return 2 * f(*xs, **kws) return aux@doubledef function(a): return 10 + aprint function(3) # Prints 26, namely 2 * (10 + 3)
With @parametrized
we can build a generic @multiply
decorator having a parameter
@parametrizeddef multiply(f, n): def aux(*xs, **kws): return n * f(*xs, **kws) return aux@multiply(2)def function(a): return 10 + aprint function(3) # Prints 26@multiply(3)def function_again(a): return 10 + aprint function(3) # Keeps printing 26print function_again(3) # Prints 39, namely 3 * (10 + 3)
Conventionally the first parameter of a parametrized decorator is the function, while the remaining arguments will correspond to the parameter of the parametrized decorator.
An interesting usage example could be a type-safe assertive decorator:
import itertools as it@parametrizeddef types(f, *types): def rep(*args): for a, t, n in zip(args, types, it.count()): if type(a) is not t: raise TypeError('Value %d has not type %s. %s instead' % (n, t, type(a)) ) return f(*args) return rep@types(str, int) # arg1 is str, arg2 is intdef string_multiply(text, times): return text * timesprint(string_multiply('hello', 3)) # Prints hellohellohelloprint(string_multiply(3, 3)) # Fails miserably with TypeError
A final note: here I'm not using functools.wraps
for the wrapper functions, but I would recommend using it all the times.