Всем привет, меня зовут Аббакумов Валерий.
Я Python разработчик, в основном занимаюсь бэкэндом веб приложений. Продолжаю серию статей для начинающих разработчиков. Мы подобрались к средне сложным примерам использования декораторов.
Как и в прошлой статье "Декораторы для самых маленьких" воду лить не буду. В этой статье будет код с краткими разъяснениями и ничего более
К сожалению обилия ссылок для детальных разъяснение в этой статье не будет, так как материал более сложный и специфичный.
Если вы чувствуйте, что данный материал пока что для вас сложен, вы всегда можете оставить комментарии или вернуться к прошлой статье.
Отправляемся к коду
Сегодня мы в основном будем крутиться вокруг использования декораторов для регистрации объектов в реестре, но рассмотрим различные синтаксические фишки
Импорты для кода данной статьи
import importlib
from collections.abc import Callable
from collections.abc import Hashable
from functools import partial, wraps
from types import MappingProxyType
from typing import Any, Annotated, cast
# Раскоментируйте для примера с pydantic
# from pydantic import BeforeValidator, AfterValidator, WrapValidator
Минимальный пример регистрации объекта класса
Чаще всего нам необходимо просто зарегистрировать объект, который мы непосредственно декорируем, поэтому задача далеко не сложная
# Объявим контейнер для регистрации объектов
_registered = []
# Функция для регистрации, по факту она просто выполняет какое-то действие и возвращает тот же самый объект
def register(cls: type):
_registered.append(cls)
return cls
# Декорируем класс и проверяем, что он попал контейнер с зарегистрированными объектами
@register
class RegisteredObject: ...
# Проверим, что объект класса добавился в наш контейнер
print(_registered)
# Консоль выведет [<class '__main__.RegisteredObject'>]
Декораторы могут быть основаны на классах
Зачастую мы хотим инкапсулировать контейнер в каком-то другом объекте
# Для этого можно использовать сервисный класс
class Register:
_registered = []
# Метод класса для регистрации объектов
@classmethod
def register(cls, target: Any):
cls._registered.append(target)
# Метод класса для получения зарегистрированных объектов
@classmethod
def get_registered(cls):
return tuple(cls._registered)
# Да, мы можем декорировать методом класса
@Register.register
class RegisteredObject: ...
# Проверим, что объект класса был зарегистрирован
print(Register.get_registered())
# Консоль выведет (<class '__main__.RegisteredObject'>,)
Еще чаще, мы хотим иметь возможность регистрировать объекты в разных "контейнерах"
class Register:
_registered: list[Any]
# Создаем контейнер для регистрации объектов в инстансе класса
def __init__(self):
self._registered = []
# Добавляем возможность регистрации через вызов инстанса класса
def __call__(self, target):
return self.register(target)
# Метод для регистрации объектов
def register(self, target: Any):
self._registered.append(target)
return target
# Свойство для получения зарегистрированных объектов
@property
def registered(self):
return tuple(self._registered)
# Создаем инстанс регистратора
first_register = Register()
# Создаем другой инстанс регистратора
second_register = Register()
# Регистрируем объект класса в первом регистраторе
@first_register.register
class RegisteredInFirstObject: ...
# Проверим, что объект класса был зарегистрирован в первом регистраторе
print(first_register.registered)
# Консоль выведет (<class '__main__.RegisteredInFirstObject'>,)
@second_register
class RegisteredInSecondObject: ...
# Проверим, что объект класса был зарегистрирован во втором регистраторе, а в первом нет
print(second_register.registered)
print(first_register.registered)
# Консоль выведет
# (<class '__main__.RegisteredInSecondObject'>,)
# (<class '__main__.RegisteredInFirstObject'>,)
# И да, мы все еще можем использовать это вместе
@first_register
@second_register
class RegisteredInBothObject: ...
# Проверим, что объект класса был зарегистрирован в обоих регистраторах
print(second_register.registered)
print(first_register.registered)
# Консоль выведет
# (..., <class '__main__.RegisteredInBothObject'>)
# (..., <class '__main__.RegisteredInBothObject'>)
@first_register
class RegisteredInFirstTwiceObject: ...
# Мы все еще можем использовать это как обычную функцию
RegisteredInFirstTwiceObject = first_register(RegisteredInFirstTwiceObject)
# Проверим, что объект класса был зарегистрирован дважды
print(first_register.registered)
# Консоль выведет
# (..., <class '__main__.RegisteredInFirstTwiceObject'>, <class '__main__.RegisteredInFirstTwiceObject'>)
Нам также ничего не мешает иметь один класс для регистрации объектов в разные "контейнеры"
class MultiRegister:
_registered: list[Any]
_another_registered: list[Any]
# Создаем контейнеры для регистрации объектов в инстансе класса
def __init__(self):
self._registered = []
self._another_registered = []
# Метод для регистрации объектов
def register(self, target: Any):
self._registered.append(target)
return target
# Метод для регистрации объектов в другом контейнере
def another_register(self, target: Any):
self._another_registered.append(target)
return target
# Свойство для получения зарегистрированных объектов
@property
def registered(self):
return tuple(self._registered)
# Свойство для получения зарегистрированных объектов в другом контейнере
@property
def another_registered(self):
return tuple(self._another_registered)
# Создаем инстанс регистратора
multi_register = MultiRegister()
# Регистрируем объект класса
@multi_register.register
class RegisteredObject: ...
# Проверим, что объект класса был зарегистрирован
print(multi_register.registered)
# Консоль выведет
# (<class '__main__.RegisteredObject'>,)
# Регистрируем объект класса в другом контейнере
@multi_register.another_register
class AnotherRegisteredObject: ...
# Проверим, что объект класса был зарегистрирован
print(multi_register.another_registered)
# Консоль выведет
# (<class '__main__.AnotherRegisteredObject'>,)
# Это тоже работает вместе
@multi_register.register
@multi_register.another_register
class MultiRegisteredObject: ...
# Проверим, что объект класса был зарегистрирован в обоих контейнерах
print(multi_register.registered)
print(multi_register.another_registered)
# Консоль выведет
# (..., <class '__main__.MultiRegisteredObject'>)
# (..., <class '__main__.MultiRegisteredObject'>)
Регистрация инстансов класса
Иногда мы хотим, чтобы регистрировались инстансы класса, который мы декорируем. Тут задача уже сложнее
# Объявим контейнер для регистрации объектов
_registered_instances = []
# Мы также можем внести изменения в сам класс
# Предположим, что мы хотим регистрировать не объекты класса, а его инстансы
def register_instances(cls: type):
# Сохраняем оригинальный метод создания нового инстанса объекта
# Это необходимо так как в области видимости функции __overridden_new__
# при обращении к cls.__new__ мы попадем в бесконечную рекурсию
__original_new__ = cls.__new__
@wraps(__original_new__)
def __overridden_new__(*args, **kwargs):
# Создаем инстанс при помощи оригинального метода
instance = __original_new__(*args, **kwargs)
# Регистрируем инстанс в нашем контейнере
_registered_instances.append(instance)
return instance
# Переопределяем функцию создания нового объекта
cls.__new__ = __overridden_new__
return cls
@register_instances
class RegisteredObjectInstances: ...
# Проверим, что объект класса не добавился в наш контейнер
print(_registered_instances)
# Консоль выведет []
# Создаем инстанс класса
RegisteredObjectInstances()
# Проверим, что инстанс класса добавлен в наш контейнер
print(_registered_instances)
# Консоль выведет [<__main__.RegisteredObject object at 0x7f0844e8c7f0>]
Реальный пример декоратора, основанного на классе
Реальный пример для тех, кто как и я часто использует pydantic (Можете считать это за превью следующей статьи, в ней что-то похожее будет минимальным по сложности примером)
Данный класс подменяет собой целевую функцию. При обращении к атрибутам before
, after
и wrap
возвращает целевую функцию, обернутую в одноименный валидатор
class _ValidatorWrapper:
# Оригинальная функция
_function: Callable
# Оригинальная функция, обернутая в BeforeValidator
_before: BeforeValidator | None = None
# Оригинальная функция, обернутая в AfterValidator
_after: AfterValidator | None = None
# Оригинальная функция, обернутая в WrapValidator
_wrap: WrapValidator | None = None
# Сохраняем оригинальную функцию
def __init__(self, function):
self._function = function
# Так как мы не хотим лишний раз что-то создавать, сделаем это, когда обратимся к свойству
# Свойство для получения BeforeValidator
@property
def before(self):
if self._before:
return self._before
self._before = BeforeValidator(self._function)
return self._before
# Свойство для получения AfterValidator
@property
def after(self):
if self._after:
return self._after
self._after = AfterValidator(self._function)
return self._after
# Свойство для получения WrapValidator
@property
def wrap(self):
if self._wrap:
return self._wrap
self._wrap = WrapValidator(self._function)
return self._wrap
# Оставим возможность использовать оригинальную функцию
def __call__(self, *args, **kwargs):
return self._function(*args, **kwargs)
# Лично мне нравится, когда декораторы именуются в snake_case
validator_wrapper = _ValidatorWrapper
# Пример декорирования
@validator_wrapper
def validator(value):
print("Validation")
return value
# Пример использования
TypeWithAnnotation = Annotated[Any, validator.before]
Заключение
В данной статье мы углубились в понимание работы декораторов, рассмотрели несколько частых примеров использования декораторов, а также рассмотрели несколько фишек того, какой синтаксис поддерживают декораторы
В следующих статьях я планирую рассказать о еще более сложных примерах, таких как:
Универсальное декорирование обычных функций, методов, методов классов, статических методов и опционально асинхронных функций
Работе с сигнатурами декорируемых функций
Декораторах, которые сами по себе являются дескрипторами
И многое другое