Класс-декоратор и декоратор класса
Эти две темы не так близки, как кажется, но я не мог разнести их в разные посты, лишая себя такого заголовка. Узнаем, как из класса сделать декоратор, и как написать декоратор для класса. Код примеров доступен в 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
Обратите внимание, что получился сразу декоратор с параметрами, где параметры – это всего лишь аргументы конструктора (нам ведь нужен экземпляр класса, а не сам класс). Поэтому если вы пишите сложный декоратор да еще и с параметрами, стоит присмотреться к его реализации классом-функтором, чтобы избежать трехэтажных вложенных функций.
Декоратор для класса
Так как в 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' выполнялся 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 # быстрый метод
Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway!
Python3. Декорирование методов внутри класса
Есть класс который содержит большое количество методов для работы с API (больше сотни), чтобы не городить для каждого метода одинаковое логирование, хочу обернуть все методы декоратором с логированием результатов выполнения метода. UPDATE: Класс содержит методы API, но так же в классе есть и служебные методы. Обернуть надо только все методы API. main.py
import logging logger = logging.getLogger("* Test *") class Test: def __init__(self): self.api = None def __test_logging(api_method): def func(*args: str, **kwargs): resp = api_method(*args, **kwargs) if resp: logging.info("<> - Success".format(args)) elif resp is None: logging.warning("<> - Not Found".format(args)) else: logging.error("<> - Unreachable".format(args)) return resp return func @ __test_logging def update_user(self, *method: str, **param: dict): resp = None # api.update_user(**param) return resp @ __test_logging def delete_user(self, *method: str, **param: dict): resp = False # api.delete_user(**param) return resp if __name__ == "__main__": test = Test() user = result = test.update_user(*"Update User", **user) print(result)
И вроде бы работает, но крайне криво. Подскажите, как правильно реализовать идею.
WARNING:root:(, 'U', 'p', 'd', 'a', 't', 'e', ' ', 'U', 's', 'e', 'r') - Not Found
P.S. Конфигурация logging находится в отдельном файле.
Декораторы методов класса в Python
Одна из отличительных особенностей Python заключается в том, что методы и функции на самом деле одинаковы. Единственное отличие состоит в том, что методы ожидают, что их первый аргумент является ссылкой на текущий объект self .
Это означает, что можно построить декоратор для методов класса на основе шаблона декоратора общего назначения!
import functools def decorate_method(method): def wrapper(*args, **kwargs): # уменьшает возраст еще больше lie = args[1] - 3 args = [args[0], lie] return method(*args, **kwargs) return wrapper class Lucy(): def __init__(self): self.age = 32 @decorate_method def sayYourAge(self, lie): print(f'Мне self.age + lie> лет, что ты об этом думаешь?') l = Lucy() l.sayYourAge(-3) # Мне 26 лет, что ты об этом думаешь?
- КРАТКИЙ ОБЗОР МАТЕРИАЛА.
- Захват аргументов функцией декоратором
- Возврат значений из декорируемой функции
- Атрибут __name__ декорируемой функции
- Шаблон декоратора общего назначения
- Декораторы с аргументами
- Вложенные декораторы
- Кэширование значений в декораторах Python
- Класс как декоратор функции Python
- Декораторы методов класса
- Декораторы классов
- Практические примеры применения декоратора Python
ХОЧУ ПОМОЧЬ
ПРОЕКТУ
Разбираемся с декораторами в Python

Декораторы – это обертка вокруг функций (или классов) в Python, которая меняет способ работы этой функции. Декоратор абстрагирует свой собственный функционал. Нотация декоратора в целом наименее инвазивна. Разработчик может писать свой код так, как ему хочется, и использовать декораторы только для расширения функциональности. Все это звучит крайне абстрактно, поэтому давайте обратимся к примерам.
В Python декораторы используются для «декорирования» функций (или методов). Возможно, один из самых популярных декораторов – это @property :
class Rectangle: def __init__(self, a, b): self.a = a self.b = b @property def area(self): return self.a * self.b rect = Rectangle(5, 6) print(rect.area) # 30
Как видно в последней строке, вы можете получить доступ к area нашего Rectangle , как к атрибуту, то есть вам не нужно вызывать метод area . Вместо этого при доступе к area , как к атрибуту (без () ), метод вызывается неявно из-за декоратора @property
Как это работает?
Написать @property перед определением функции – то же самое, что написать
area = property(area) . Другими словами: property – это функция, которая принимает другую функцию в качестве аргумента и возвращает третью. Так и ведут себя декораторы.
В результате декораторы изменяют поведение функции, к которой они применяются.
Пишем свои декораторы
Декоратор retry
Давайте по этому расплывчатому определению напишем свои декораторы, чтобы понять, как они работают.
Допустим, у нас есть функция, выполнение которой мы хотим повторить, если она завершится неудачно. Нам нужна функция (декоратор), которая вызовет нашу функцию один или два раза (в зависимости от того, как функция завершится в первый раз).
С учетом нашего изначального определения декоратора мы можем написать простой декоратор следующим образом:
def retry(func): def _wrapper(*args, **kwargs): try: func(*args, **kwargs) except: time.sleep(1) func(*args, **kwargs) return _wrapper @retry def might_fail(): print("might_fail") raise Exception might_fail()
Retry – имя нашего декоратора, который принимает в качестве аргумента любую функцию ( func ). Внутри декоратора определяется и возвращается новая функция ( _wrapper ). На первый взгляд определение одной функции внутри другой может показаться несколько непривычным. Однако синтаксически это совершенно нормально и имеет определенное преимущество, ведь функция _wrapper существует только внутри пространства имен нашего декоратора retry .
Обратите внимание, что в примере мы отдекорировали нашу функцию только с помощью @retry . После декоратора @retry нет круглых скобок. Таким образом, при вызове функции might_fail() декоратор @retry вызовется с нашей функцией ( might_fail ) в качестве первого аргумента.
В итоге мы обрабатываем три функции:
Иногда нужно, чтобы декоратор принимал аргументы. В нашем случае мы можем сделать параметром количество повторных попыток. Однако декоратор должен принять нашу функцию в качестве первого аргумента. Вспомните, нам не нужно было вызывать декоратор при декорировании функции с его помощью, то есть мы просто писали @retry , а не @retry() .
- Декоратор – не что иное, как функция (которая принимает другую функцию в качестве аргумента)
- Чтобы использовать декоратор, нужно поместить его перед определением функции без ее вызова
Следовательно мы могли бы написать четвертую функцию, которая принимает нужный нам параметр в качестве конфигурации и возвращает функцию, которая на самом деле и есть декоратор (который принимает другую функцию в качестве аргумента).
Давайте попробуем так:
def retry(max_retries): def retry_decorator(func): def _wrapper(*args, **kwargs): for _ in range(max_retries): try: func(*args, **kwargs) except: time.sleep(1) return _wrapper return retry_decorator @retry(2) def might_fail(): print("might_fail") raise Exception might_fail()
Разложим на составляющие:
- Сначала у нас была функция retry ;
- Retry принимает произвольный аргумент ( max_retries в нашем случае) и возвращает функцию;
- retry_decorator — это функция, возвращаемая retry и по факту наш декоратор;
- _wrapper работает так же, как и раньше (теперь он просто выполняет максимальное количество попыток).
Для определения нашего декоратора:
- На этот раз might_fail декорируется вызовом функции, т.е. @retry(2) ;
- retry(2) вызывает retry , а та возвращает сам декоратор;
- might_fail в конечном итоге будет отдекорирована retry_decorator , поскольку эта функция является результатом вызова retry(2) .
Декоратор-таймер
Вот еще один пример полезного декоратор. Давайте напишем декоратор, который будет возвращать время выполнения функций.
import functools import time def timer(func): @functools.wraps(func) def _wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) runtime = time.perf_counter() - start print(f" took secs") return result return _wrapper @timer def complex_calculation(): """Some complex calculation.""" time.sleep(0.5) return 42 print(complex_calculation())
complex_calculation took 0.5041 secs 42
Как мы видим, декоратор-таймер выполняет код до и после функции и работает точно так же, как и в последнем примере.
functools.wraps
Возможно, вы заметили, что сама функция _wrapper отдекорирована @functools.wraps . Этот факт никак не меняет логику или функциональность нашего декоратора-таймера. С таким же успехом вы могли бы вообще не использовать @functools.wraps .
Однако, поскольку наш декоратор @timer мог быть написан как: complex_calculation = timer(complex_calculation) , декоратор обязательно изменит нашу функцию complex_calculation . В частности, он меняет некоторые атрибуты специальных методов:
- __module__
- __name__
- __qualname__
- __doc__
- __annotations__
При использовании @functools.wraps все эти атрибуты возвращаются к значениям по умолчанию.
Без @functools.wraps :
print(complex_calculation.__module__) # __main__ print(complex_calculation.__name__) # wrapper_timer print(complex_calculation.__qualname__) # timer..wrapper_timer print(complex_calculation.__doc__) # None print(complex_calculation.__annotations__) # <>
С @functools.wraps :
print(complex_calculation.__module__) # __main__# print(complex_calculation.__name__) # complex_calculation print(complex_calculation.__qualname__) # complex_calculation print(complex_calculation.__doc__) # Some complex calculation. print(complex_calculation.__annotations__) # <>
Декораторы классов
До настоящего момента мы рассматривали декораторы функций. Однако их можно также использовать для классов.
Давайте возьмем наш таймер из примера выше. Мы совершенно спокойно сможем обернуть в него класс:
@timer class MyClass: def complex_calculation(self): time.sleep(1) return 42 my_obj = MyClass() my_obj.complex_calculation()
Finished 'MyClass' in 0.0000 secs
Очевидно, что выполнение complex_calculation не заняло времени. Помните, что @нотация — это просто эквивалент записи MyClass = timer(MyClass) , т.е. декоратор будет вызван только тогда, когда вы «вызовете» класс. Вызов класса означает создание его экземпляра, поэтому таймер отработает только для строки my_obj = MyClass() .
Методы класса не декорируются автоматически при декорации класса. Проще говоря, при использовании декоратора для обычного класса декорируется только его конструктор (метод __init__ ).
Вы можете изменить поведение класса в целом, используя другую форму конструктора. Однако давайте сначала посмотрим, могут ли декораторы работать наоборот, то есть можем ли мы отдекорировать функцию классом. Оказывается, можем:
class MyDecorator: def __init__(self, function): self.function = function self.counter = 0 def __call__(self, *args, **kwargs): self.function(*args, **kwargs) self.counter+=1 print(f"Called times") @MyDecorator def some_function(): return 42 some_function() some_function() some_function()
Called 1 times Called 2 times Called 3 times
Как это работает:
- __init__ вызывается при оформлении some_function . Опять же, помните, что декорирование — это то же самое, что some_function = MyDecorator(some_function) .
- __call__ вызывается, когда используется экземпляр класса, например, при вызове функции. Поскольку some_function теперь является экземпляром моего декоратора, но мы все еще хотим использовать ее как функцию, нам понадобится специальный метод __call__ .
Декорирование класса в Python работает как изменение класса извне (т.е. из декоратора).
def add_calc(target): def calc(self): return 42 target.calc = calc return target @add_calc class MyClass: def __init__(): print("MyClass __init__") my_obj = MyClass() print(my_obj.calc())
MyClass __init__ 42
Опять же, вспомним определение декоратора, ведь все, что здесь происходит, следует той же логике:
- my_obj = MyClass() сначала вызывает декоратор,
- Декоратор add_calc добавляет метод calc к классу
- В итоге класс создается с помощью конструктора.
Вы можете использовать декораторы для изменения классов подобно наследованию. Хорошо это или плохо в значительной степени зависит от архитектуры вашего проекта в целом. Декоратор dataclass из стандартной библиотеки — отличный пример разумного использования, при котором декораторы предпочтительнее наследования. Мы сейчас поговорим об этом.
Использование декораторов
Декораторы в стандартной библиотеке Python
В следующих разделах мы познакомимся с некоторыми популярными и полезными декораторами, которые уже есть в стандартной библиотеке.
Property
Как мы уже знаем, декоратор @property , вероятно, один из наиболее часто используемых декораторов в Python. Он нужен, чтобы вы могли получить доступ к результату выполнения метода как к атрибуту. Конечно, существует аналог @property , с помощью которого вы можете вызывать метод под капотом при выполнении операции присваивания.
class MyClass: def __init__(self, x): self.x = x @property def x_doubled(self): return self.x * 2 @x_doubled.setter def x_doubled(self, x_doubled): self.x = x_doubled // 2 my_object = MyClass(5) print(my_object.x_doubled) # 10 print(my_object.x) # 5 my_object.x_doubled = 100 # print(my_object.x_doubled) # 100 print(my_object.x) # 50
Staticmethod
Еще один популярный декоратор – staticmethod . Он нужен, если вы хотите вызвать функцию, определенную внутри класса не создавая экземпляр класса:
class C: @staticmethod def the_static_method(arg1, arg2): return 42 print(C.the_static_method())
functools.cache
Когда вы имеете дело с функциями, которые выполняют сложные вычисления, вам может понадобиться закэшировать их результат.
Можно сделать что-то вроде этого:
_cached_result = None def complex_calculations(): if _cached_result is None: _cached_result = something_complex() return _cached_result
Сохранение глобальной переменной, такой как _cached_result , проверка ее на None и помещение результата в эту переменную являются повторяющимися задачами. Все это делает их идеальным кандидатом на должность декоратора. К счастью, в стандартной библиотеке Python есть декоратор, который сделает это все за нас:
from functools import cache @cache def complex_calculations(): return something_complex()
Теперь каждый раз, когда вы вызываете complex_calculations() , Python сначала проверяет наличие кэшированного результата, прежде чем вызывать something_complex . Если в кэше есть результат, something_complex не будет вызываться дважды.
Dataclasses
В разделе про декораторы классов мы видели, что их можно использовать для изменения поведения классов по аналогии с наследованием.
Модуль dataclasses в стандартной библиотеке является хорошим примером, когда использование декоратора предпочтительнее, чем использование наследования. Давайте сначала посмотрим, как использовать dataclasses :
from dataclasses import dataclass @dataclass class InventoryItem: name: str unit_price: float quantity: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity item = InventoryItem(name="", unit_price=12, quantity=100) print(item.total_cost()) # 1200
На первый взгляд, декоратор @dataclass добавил только конструктор, поэтому мы избегали шаблонного кода, подобного этому:
. def __init__(self, name, unit_price, quantity): self.name = name self.unit_price = unit_price self.quantity = quantity .
Однако, если вы решите написать REST-API для своего проекта на Python и вам нужно преобразовать ваши объекты Python в строки JSON, для есть пакет под названием dataclasses-json (отсутствует в стандартной библиотеке), который декорирует классы и обеспечивает сериализацию и десериализацию объектов в строки JSON и наоборот.
Давайте посмотрим, как это выглядит:
from dataclasses import dataclass from dataclasses_json import dataclass_json @dataclass_json @dataclass class InventoryItem: name: str unit_price: float quantity: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity item = InventoryItem(name="", unit_price=12, quantity=100) print(item.to_dict()) #
Отсюда можно сделать два вывода:
- Декораторы могут быть вложенными. Важен порядок их написания.
- Декоратор @dataclass_json добавил в наш класс метод to_dict
Конечно, мы могли бы написать класс-примесь, который выполняет тяжелую работу по реализации метода to_dict , безопасный для типов данных, а затем наследовать класс InventoryItem от него.
Однако в данном случае декоратор добавляет только техническую функциональность (в отличие от расширения функционала). В результате мы можем просто «включать и выключать» декоратор без изменения поведения нашего приложения. «Естественная» иерархия классов сохраняется, и никаких изменений в код вносить не нужно. Мы можем добавить декоратор dataclasses-json в проект без изменения уже готовых методов.
В таком случае изменение класса с помощью декоратора выглядит элегантнее (потому что оно сохраняет модульность), чем наследование или использование примесей.
Завтра в OTUS состоится открытое занятие «Docker для Python разработчика». Рассмотрим best practices написания Dockerfile’ов и работы с docker’ом в целом. Обсудим нюансы как общего характера, так и Python-специфичные. Регистрация — здесь.