Search code examples
pythonpython-decorators

Merging Python decorators with arguments into a single one


I'm using two different decorators from two different libraries. Lets say: @decorator1(param1, param2) and @decorator2(param3, param4). That I often use in many functions as:

from moduleA import decorator1
from moduleB import decorator2

@decorator2(foo='param3', bar='param4')
@decorator1(name='param1', state='param2')
def myfunc(funcpar1, funcpar2):
    ...

Since it happens every time, I would like to create a custom decorator that handle both of them. Something like:

@mycustomdecorator(name='param1', state='param2',
                   foo='param3', bar='param4')
def myfunc(funcpar1, funcpar2):
    ...

How do I achieve that?


Solution

  • I would argue that you shouldn't - using the original names for the decorators gives much better readability.

    However, if you really want to, you can do it like this:

    import functools
    
    from moduleA import decorator1
    from moduleB import decorator2
    
    def my_decorator(foo, bar, name, state):
        def inner(func):
            @decorator2(foo=foo, bar=bar)
            @decorator1(name=name, state=state)
            @functools.wraps(func)  # Not required, but generally considered good practice
            def newfunc(*args, **kwargs)
                return func(*args, **kwargs)
            return newfunc
        return inner
    
    @my_decorator(foo='param3', bar='param4', name='param1', state='param2')
    def myfunc(funcpar1, funcpar2):
        ...
    

    Based on comments though, here's an alternative method:

    def my_decorator(foo, bar, name, state):
        def inner(func):
            # Please note that for the exact same result as in the other one, 
            # the order of decorators has to be reversed compared to normal decorating
            newfunc = decorator1(name=name, state=state)(func)
            newfunc = decorator2(foo=foo, bar=bar)(newfunc)
            # Note that functools.wraps shouldn't be required anymore, as the other decorators should do that themselves
            return newfunc
        return inner
    

    To some, this might look simpler. However, people experienced with Python are used to decorators being applied with an @ - and even for that reason alone, I like my first option better. I know I'd take three times as long to read this code for the first time and understand what it does.

    It's simple really - just write a decorator that returns another decorator which will have it's inner function decorated with the other two decorators ;)

    It might also be a good idea to use functools.wraps, for the sake of good habits. It's standard library and helps a lot with debugging and interactive console use: https://docs.python.org/3.7/library/functools.html

    In general though, I'd say the one extra line of code is more than worth the clarity of using the decorators separately. You'll thank yourself when you read your own code in 3 more months.