Шаг 159.
Python: сборник рецептов.
Метапрограммирование. Определение декоратора, принимающего аргументы

    На этом шаге мы рассмотрим решение этой задачи.

Задача

    Вы хотите создать функцию-декоратор, которая принимала бы аргументы.

Решение

    Давайте покажем процесс приема аргументов на примере. Предположим, вы хотите написать декоратор, который добавляет к функции логирование и при этом позволяет пользователю указать уровень логирования и прочие параметры через аргументы. Вот как вы можете это сделать:

>>> from functools import wraps
>>> import logging
>>> def logged(level, name=None, message=None):
    '''
    Добавляет логирование в функцию. level - это уровень логирования, 
    name - это название логгера, message - это сообщение в лог. 
    Если name и message не определены, они будут дефолтными 
    от имени функции и ее модуля.
    '''
    def decorate(func):
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
    return decorate

>>> # Пример использования
>>> @logged(logging.DEBUG)
def add(x, y):
    return x + y

>>> @logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')

>>> 
>>> logging.basicConfig(level=logging.DEBUG)
>>> print(add(2, 3))
DEBUG:__main__:add
5
>>> spam()
CRITICAL:example:spam
Spam!
>>> 

    На первый взгляд реализация выглядит нетривиальной, но идея относительно проста. Самая внешняя функция logged() принимает желаемые аргументы и просто делает их доступными внутренним функциям декоратора. Внутренняя функция decorate() принимает функцию и помещает ее в обертку обычным образом. Ключевой момент в том, что обертка может использовать аргументы, переданные в logged().

Обсуждение

    Создавать декораторы, принимающие аргументы, довольно сложно, поскольку они используют "подкапотную" последовательность вызовов. Говоря конкретно, если у вас есть вот такой код:

@decorator(x, y, z) 
def func(a, b): 
    pass
то процесс декорирования будет идти так:
def func(a, b): 
    pass

func = decorator(x, y, z)(func)

    Обратите внимание, что результат вызова decorator(x, y, z) должен быть вызываемым объектом, который, в свою очередь, принимает функцию и оборачивает ее.

    На следующем шаге мы рассмотрим определение декоратора с настраиваемыми пользователем атрибутами.




Предыдущий шаг Содержание Следующий шаг