Decorators with parameters? Decorators with parameters? python python

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.