
Эти две темы не так близки, как кажется, но я не мог разнести их в разные посты, лишая себя такого заголовка. Узнаем, как из класса сделать декоратор, и как написать декоратор для класса. Код примеров доступен в GIST под каждым из разделов.
Класс как декоратор
Если у класс реализовать магический метод __call__
, то экземпляр такого класса можно будет вызывать как функцию, при этом, очевидно, будет вызываться просто этот самый магический метод. Такой объект называют функтором. Пример:
class Functor: def __call__(self, a, b): print(a * b) f = Functor() # вызов как будто функция f(10, 20)
Как мы помним из https://tirinox.ru/parametric-decorator/ , справа от собачки в декораторе может стоять не только функция-декоратор, но любой вызываемый объект, например, функтор. __call__
, которого будет принимать на вход единственный параметр – декорируемую функцию. На примере того же декоратора-повторителя вызовов:
from functools import wraps class Repeater: def __init__(self, n): self.n = n def __call__(self, f): @wraps(f) def wrapper(*args, **kwargs): for _ in range(self.n): f(*args, **kwargs) return wrapper @Repeater(3) def foo(): print('foo') foo() # foo # foo # foo
Обратите внимание, что получился сразу декоратор с параметрами, где параметры – это всего лишь аргументы конструктора (нам ведь нужен экземпляр класса, а не сам класс). Поэтому если вы пишите сложный декоратор да еще и с параметрами, стоит присмотреться к его реализации классом-функтором, чтобы избежать трехэтажных вложенных функций.
Код здесь https://gist.github.com/tirinox/b6fd34de1b9de229ec2666f160c1ad82.
Декоратор для класса
Так как в Python классы создаются динамически по время интерпретации исходного кода, то можно влиять на этот процесс, например, путем декорирования. Аналогично декораторам функций, декоратор класса призван модифицировать поведение и содержание класса, не изменяя его исходный код. Похоже на наследование, но есть отличия:
- Декоратор класса имеет более глубокие возможности по влиянию на класс, он может удалять, добавлять, менять, переименовывать атрибуты и методы класса. Он может возвращать совершенно другой класс.
- Старый класс «затирается» и не может быть использован, как базовый класс при полиморфизме
- Декорировать можно любой класс одним и тем же универсальный декоратором, а при наследовании – мы ограничены иерархией классов и должны считаться с интерфейсами базовых классов.
- Презираются все принципы и ограничения ООП (из-за пунктов 1-3).
Декораторы классов полезны, чтобы внедриться в класс (иногда незаметно) и массово воздействовать на его методы и атрибуты. Типичный пример – создадим декоратор, который будет измерять время выполнения каждого метода класса. При этом сам класс никаких изменений не претерпит и не будет знать, что за ним следят:
import time # это вспомогательный декоратор будет декорировать каждый метод класса, см. ниже def timeit(method): def timed(*args, **kw): ts = time.time() result = method(*args, **kw) te = time.time() delta = (te - ts) * 1000 print(f'{method.__name__} выполнялся {delta:2.2f} ms') return result return timed def timeit_all_methods(cls): class NewCls: def __init__(self, *args, **kwargs): # проксируем полностью создание класса # как создали этот NewCls, также создадим и декорируемый класс self._obj = cls(*args, **kwargs) def __getattribute__(self, s): try: # папа, у меня есть атрибут s? x = super().__getattribute__(s) except AttributeError: # нет сынок, это не твой атрибут pass else: # да сынок, это твое return x # объект, значит у тебя должен быть атрибут s attr = self._obj.__getattribute__(s) # метод ли он? if isinstance(attr, type(self.__init__)): # да, обернуть его в измеритель времени return timeit(attr) else: # не метод, что-то другое return attr return NewCls @time_all_class_methods class Foo: def a(self): print("метод a начался") time.sleep(0.666) print("метод a кончился") f = Foo() f.a() # метод a начался # метод a кончился # a 668.74 ms
Рассмотрим подробно части кода. timeit
– это простой декоратор для функций, мы его уже умеем делать. Он нужен для того, чтобы декоратор класса timeit_all_methods
обернул в timeit
каждый метод декорируемого класса.
Декоратор timeit_all_methods
содержит в себе определение нового класса NewCls
и возвращает его вместо оригинального класса. Т.е. класс Foo
– это уже не Foo
, а NewCls
. Конструктор класса NewCls
принимает произвольные аргументы (ведь нам не известно заранее, какой конструктор у Foo
, и у любого другого класса, который мы декорируем). Поэтому конструктор просто создает поле, где будет хранить экземпляр оригинального класса, и передает ему в конструктор все свои аргументы.
Самый сложный метод – __getattribute__ – он полон магии. Он вызывается, когда кто-то пытается обратиться как какому угодно атрибуту (полю, методы и т. п.) класса NewCls
. Первым делом мы должны обратиться к своему родителю super()
и спросить у него, не обладаем ли мы сами атрибутом, который проверяем. Именно к родителю, чтобы избежать рекурсии (иначе мы попадем в тот же метод, в котором уже находимся)! Если это наш атрибут (атрибут класса декоратора) – вернем его сразу, с ним ничего не надо делать. Иначе, вероятно, это атрибут исходного класса – получим его у него. И проверим его тип, сравним его с типом любого метода. Если тип – метод (bound method), то обернем его в декоратор timeit
и вернем, иначе (это не метод, а свойство или статический метод) – вернем без изменений.
Таким образом мы проксируем все атрибуты обернутого класса через NewCls
, оборачивая в timeit
только методы.
Задание на дом: создать класс декоратор класса, иначе говоря скрестить два раздела статьи и сделать класс-функтор, который может декорировать другой класс. Идея: декоратор, который измеряет время выполнения каждого метода, и печатает предупреждение, только если время выполнения было больше критического (параметр):
@TimeItCritical(critical_time=0.3) class Foo: def a(self): print("медленный метод начался") time.sleep(1.0) print("медленный метод кончился") def b(self): time.sleep(0.1) print('быстрый метод') f = Foo() f.a() f.b() # медленный метод начался # медленный метод кончился # a выполнялся медленно 1.0011 s # быстрый метод
Код доступен в https://gist.github.com/tirinox/507258b36e77dfec1448f8cf1d259356
🤩 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway! 👈