На этом шаге мы рассмотрим особенности такого определения декораторов.
Вы хотите оборачивать функции декоратором, но результат должен быть вызываемым объектом. Вы хотите, чтобы ваш декоратор работал и внутри, и снаружи определения класса.
Чтобы определить декоратор как экземпляр, вы должны убедиться, что в нем реализованы методы __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 >>>
На следующем шаге мы рассмотрим применение декораторов к методам класса и статическим методам.