Декораторы классов: изменение поведения классов с помощью обёрток
Что такое декораторы классов и зачем они нужны? 🎭
Декораторы классов — это мощный инструмент Python, позволяющий динамически изменять поведение классов, не трогая их исходный код. Они работают по тому же принципу, что и декораторы функций, но применяются к классам.
Основные сценарии использования:
- Добавление новой функциональности (логирование, кэширование)
- Изменение или переопределение методов
- Регистрация классов в системе (например, для плагинов)
- Валидация или проверка атрибутов
def decorator(cls):
# Здесь модифицируем класс
return cls
@decorator
class MyClass:
pass
Как работают декораторы классов под капотом 🔧
Когда вы применяете декоратор к классу, Python делает следующее: 1. Создаёт исходный класс 2. Передаёт его в декоратор как аргумент 3. Заменяет исходный класс на результат работы декоратора
Пример простейшего декоратора, который добавляет метод:
def add_method(cls):
def greet(self):
return f"Hello from {cls.__name__}!"
cls.greet = greet
return cls
@add_method
class Person:
pass
p = Person()
print(p.greet()) # Hello from Person!
Реальный пример: декоратор для синглтона 🏆
Практическое применение — реализация паттерна Singleton:
def singleton(cls):
instances = {}
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class Database:
def __init__(self):
print("Подключение к БД...")
db1 = Database()
db2 = Database()
print(db1 is db2) # True - это один и тот же объект!
Декоратор с параметрами 🎛️
Декораторы классов могут принимать параметры для гибкой настройки:
def add_attributes(**kwargs):
def decorator(cls):
for key, value in kwargs.items():
setattr(cls, key, value)
return cls
return decorator
@add_attributes(version="1.0", author="Danila Bezhin")
class Calculator:
pass
print(Calculator.version) # 1.0
print(Calculator.author) # Danila Bezhin
Комбинирование декораторов ⚡
Декораторы можно накладывать последовательно:
def log_creation(cls):
original_init = cls.__init__
def wrapped_init(self, *args, **kwargs):
print(f"Создан экземпляр {cls.__name__}")
original_init(self, *args, **kwargs)
cls.__init__ = wrapped_init
return cls
@singleton
@log_creation
class Configuration:
pass
config = Configuration() # Выведет: "Создан экземпляр Configuration"
Когда использовать, а когда нет? ⚖️
Хорошие кандидаты для декораторов классов: - Добавление метаданных - Регистрация классов - Модификация поведения для всех экземпляров - Паттерны проектирования (Singleton, Factory)
Когда лучше отказаться: - Когда изменения слишком сложные (лучше использовать наследование) - Когда важно сохранить читаемость и прозрачность кода - Для простых задач, где можно обойтись обычными методами
Продвинутый пример: валидатор атрибутов 🔍
Декоратор, который проверяет типы атрибутов класса:
def validate_attributes(**validators):
def decorator(cls):
original_init = cls.__init__
def wrapped_init(self, *args, **kwargs):
for name, validator in validators.items():
if name in kwargs:
if not validator(kwargs[name]):
raise ValueError(f"Недопустимое значение для {name}")
original_init(self, *args, **kwargs)
cls.__init__ = wrapped_init
return cls
return decorator
@validate_attributes(
age=lambda x: x >= 0,
name=lambda x: isinstance(x, str) and x
)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
try:
p = Person(name="", age=-5) # Вызовет два ValueError!
except ValueError as e:
print(e)
Важные нюансы работы с декораторами 🧐
- Порядок декораторов имеет значение — они применяются снизу вверх
- Исходный класс заменяется полностью — могут потеряться некоторые метаданные
- Интроспекция (например,
help()) может работать неожиданно - Для сложных сценариев лучше использовать метаклассы
Практическое задание ✏️
Создайте декоратор класса @timer, который добавляет в класс метод measure(), замеряющий время выполнения любого метода класса:
@timer
class MyClass:
def long_running_method(self):
import time
time.sleep(2)
obj = MyClass()
obj.measure('long_running_method')
# Должен вывести время выполнения метода
Подсказка: используйте модуль time и functools.wraps для сохранения метаданных методов.