Шаг 160.
Python: сборник рецептов. Метапрограммирование. Определение декоратора с настраиваемыми пользователем атрибутами

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

Задача

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

Решение

    Это решение расширяет предыдущий рецепт путем введения функций доступа (акцессоров), которые меняют внутренние переменные через объявление нелокальных переменных (nonlocal). Функции доступа затем прикрепляются к функции-обертке как атрибуты функции.

>>> from functools import wraps, partial
>>> import logging
>>> # Вспомогательный декоратор для прикрепления
>>> # к функции в качестве атрибута obj
>>> def attach_wrapper(obj, func=None):
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

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

        # Прикрепляем функции-сеттеры
        @attach_wrapper(wrapper)
        def set_level(newlevel):
            nonlocal level
            level = newlevel

        @attach_wrapper(wrapper)
        def set_message(newmsg):
            nonlocal logmsg
            logmsg = newmsg

        return wrapper
    return decorate

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

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

>>> 

    Вот интерактивный сеанс, который демонстрирует изменения различных атрибутов после определения:

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> add(2, 3)
DEBUG:__main__:add
5
>>> # # Изменение сообщения в лог
>>> add.set_message('Add called')
>>> add(2, 3)
DEBUG:__main__:Add called
5
>>> # Change the log level
>>> add.set_level(logging.WARNING)
>>> add(2, 3)
WARNING:__main__:Add called
5
>>>


Обсуждение

    Ключевой момент этого рецепта заключается в функциях доступа (т. е. set_message() и set_level()), которые прикрепляются к обертке в качестве атрибутов. Каждая из этих функций доступа (акцессоров) позволяет изменять внутренние параметры путем использования присвоений через nonlocal.

    Удивительная возможность этого рецепта в том, что функции доступа будут распространяться через несколько уровней декорирования (если все ваши декораторы используют @functools.wraps). Предположим, например, что вы ввели дополнительный декоратор, такой как @timethis (см. 157 шаг), и написали вот такой код:

>>> @timethis
@logged(logging.DEBUG)
def countdown(n):
    while n > 0:
        n -= 1

        
>>> 

    Вы обнаружите, что методы доступа все еще работают:

>>> import time
>>> from functools import wraps
>>> countdown(100000)
DEBUG:__main__:countdown
countdown 0.018980741500854492
>>> countdown.set_level(logging.WARNING)
>>> countdown.set_message("Counting down to zero")
>>> countdown(10000000)
WARNING:__main__:Counting down to zero
countdown 1.0781216621398926
>>> 

    Вы также обнаружите, что они работают именно так, как если бы декораторы были указаны в обратном порядке:

@logged(logging.DEBUG)
@timethis 
def countdown(n): 
    while n > 0: 
        n -= 1

    Хотя это не показано, функции доступа, возвращающие различные настройки, могут быть легко определены путем добавления вот такого кода:

.  .  .
@attach_wrapper(wrapper) 
def get_level(): 
    return level

# Альтернатива
wrapper.get_level = lambda: level
.  .  .

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

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

# Прикрепление настраиваемых атрибутов 
wrapper.level = level 
wrapper.logmsg = logmsg 
wrapper.log = log
.  .  .

    Этот подход будет работать до определенной точки, но только при использовании с декоратором верхнего уровня. Если у вас будет другой декоратор, добавленный поверх (такой как @timethis в этом примере), он заслонит лежащие ниже атрибуты и сделает их недоступными для изменения. Использование функций доступа позволяет обойти это ограничение.

    И последнее: показанное в этом рецепте решение может стать альтернативой определения декораторов как классов.

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




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