Шаг 164.
Python: сборник рецептов.
Метапрограммирование. Определение декораторов как классов

    На этом шаге мы рассмотрим особенности такого определения декораторов.

Задача

    Вы хотите оборачивать функции декоратором, но результат должен быть вызываемым объектом. Вы хотите, чтобы ваш декоратор работал и внутри, и снаружи определения класса.

Решение

    Чтобы определить декоратор как экземпляр, вы должны убедиться, что в нем реализованы методы __call__() и __get__(). Например, этот код определяет класс, который оборачивает функцию простым профилирующим слоем:

>>> import types
>>> from functools import wraps
>>> class Profiled:
    def __init__(self, func):
        wraps(func)(self)
        self.ncalls = 0

    def __call__(self, *args, **kwargs):
        self.ncalls += 1
        return self.__wrapped__(*args, **kwargs)

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

>>> 

    Чтобы использовать этот класс, примените его как обычный декоратор - снаружи или внутри класса:

>>> @Profiled
def add(x, y):
    return x + y

>>> class Spam:
    @Profiled
    def bar(self, x):
        print(self, x)

>>> 

    Вот интерактивный сеанс, который показывает, как работают эти функции:

>>> add(2, 3)
5
>>> add(4, 5)
9
>>> add.ncalls
2
>>> s = Spam()
>>> s.bar(1)
<__main__.Spam object at 0x000001C3FD5584C0> 1
>>> s.bar(2)
<__main__.Spam object at 0x000001C3FD5584C0> 2
>>> s.bar(3)
<__main__.Spam object at 0x000001C3FD5584C0> 3
>>> Spam.bar.ncalls
3
>>> 


Обсуждение

    Определение декоратора как класса не вызывает особых сложностей. Однако есть несколько тонких аспектов, которые заслуживают быть объясненными, - особенно если вы планируете применить декоратор к методам экземпляров.

    Во-первых, использование функции functools.wraps() здесь служит той же цели, что и в обычных декораторах, а именно копированию важных метаданных из обернутой функции в вызываемый экземпляр.

    Во-вторых, часто забывают про метод __get__(), показанный в решении. Если вы опустите __get__() и оставите код без изменений, то при попытке вызвать декорированные методы экземпляра обнаружите странные вещи. Например:

>>> s = Spam()
>>> s.bar(3)
Traceback (most recent call last):
.  .  .
TypeError: spam() missing 1 required positional argument: 'x'

    Причина поломки в том, что когда функции, реализующие методы, обращаются к классу, и их метод __get__() вызывается как часть протокола дескриптора, который описан на 138 шаге. В этом случае назначение метода __get__() - в создании связанного объекта метода (который поставляет аргумент self методу). Вот пример, который иллюстрирует лежащую в основе механику:

>>> s = Spam()
>>> def grok(self, x):
	pass

>>> grok.__get__(s, Spam)
<bound method grok of <__main__.Spam object at 0x000001C3FD5585B0>>
>>>

    В этом рецепте метод __get__() нужен, чтобы убедиться, что связанные объекты методов создаются правильно. type.MethodType() здесь создает связанный метод вручную. Связанные методы создаются только в том случае, если экземпляр используется. Если метод запрашивает доступ к классу, аргументу instance функции __get__() присваивается значение None, и возвращается сам экземпляр Profiled. Это делает возможным извлечение атрибута его атрибута ncalls, что и было показано.

    Если вы не хотите возиться с этим беспорядком, рассмотрите альтернативную реализацию декоратора, использующую замыкания и переменные с объявлением nonlocal, как показано на 160 шаге. Например:

>>> import types
>>> from functools import wraps
>>> def profiled(func):
    ncalls = 0
    @wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal ncalls
        ncalls += 1
        return func(*args, **kwargs)
    wrapper.ncalls = lambda: ncalls
    return wrapper

>>> # Пример
>>> @profiled
def add(x, y):
    return x + y

>>> 

    Этот пример работает практически точно так же - за исключением того, что доступ к ncalls теперь предоставляется через функцию, прикрепленную к функции в качестве атрибута. Например:

>>> add(2, 3)
5
>>> add(4, 5)
9
>>> add.ncalls()
2
>>> 

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




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