О слиянии нескольких декораторов с аргументами в один

Предположим, есть пара декораторов из разных библиотек: @decorator1(param1, param2) и @decorator2(param3, param4). Декораторы часто использутся в паре:

from moduleA import decorator1
from moduleB import decorator2

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

Хотелось бы иметь декоратор, объединяющий два предыдущих:

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

Как сделать такой декоратор? И стоит ли игра свеч?

Вообще говоря, так не стоит делать. Использование оригинальных имен декораторов дает гораздо большую читаемость.

Но если все же хочется объединить пару декораторов, можно поступить следующим образом:

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):
    ...

Альтернативный способ:

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

Для некоторых последний вариант может выглядеть проще. Тем не менее, люди, имеющие опыт работы с Python, применяют декораторы, используя @. По этой причине первый вариант предпочтительнее. Совершенно точно, что понадобится трижды больше времени, чтобы прочитать этот код в первый раз и понять, что он делает.

А так все просто: пишем декоратор, который возвращает другой декоратор, имеющий внутреннюю функцию, обернутую двумя другими декораторами.

Также хорошая практика — использовать functools.wraps. Это стандартная библиотека и сильно помогает в отладке и интерактивной консоли: https://docs.python.org/3.7/library/functools.html.

Вообще говоря, одна дополнительная строка с декоратором — совершенно небольшая плата за ясность. Подумайте. Вы будете благодарить себя, когда прочитаете собственный код спустя несколько месяцев.


По материалам Jacco van Dorp.