Классы-декораторы в Python
Декораторы-классы в Python: __call__, передача аргументов через __init__, functools.wraps и сравнение с функциями-декораторами.
Декоратор-класс — это класс, экземпляр которого является вызываемым (callable). Чтобы экземпляр вёл себя как функция, достаточно определить метод
__call__. Аргументы декоратора передаются в__init__, а декорируемая функция — в__call__.
Почему именно класс, а не функция
Функция-декоратор хороша для простых случаев. Но когда нужно хранить состояние между вызовами, вести счётчик или поддерживать конфигурацию — класс удобнее: данные живут в атрибутах self.
Простейший декоратор-класс
Начнём с декоратора без аргументов — он оборачивает функцию и выводит «звёздочки»:
class Stars:
def __init__(self, fn):
self.fn = fn # сохраняем функцию как атрибут
def __call__(self, *args, **kwargs):
print("*" * 10)
result = self.fn(*args, **kwargs)
print(result)
print("*" * 10)
return result
@Stars
def greet(name):
return f"Привет, {name}!"
greet("Алиса")
Вывод:
********** Привет, Алиса! **********
При применении @Stars Python вызывает Stars(greet) — то есть __init__ получает декорируемую функцию напрямую.
Декоратор с аргументами
Если декоратор принимает аргументы (@Stars(5)), схема немного другая: __init__ получает аргументы, а __call__ — декорируемую функцию и должен вернуть обёртку:
from functools import wraps
class Stars:
def __init__(self, n):
self.n = n # аргумент декоратора
def __call__(self, fn):
@wraps(fn) # сохраняем __name__, __doc__ оригинала
def wrapper(*args, **kwargs):
print(self.n * "*")
result = fn(*args, **kwargs)
print(result)
print(self.n * "*")
return result
return wrapper
@Stars(5)
def add(a, b):
return a + b
add(10, 20)
Вывод:
***** 30 *****
Конструкция @Stars(5) эквивалентна:
add = Stars(5)(add)
Stars(5)→ создаёт экземпляр,self.n = 5.instance(add)→ вызывает__call__(add)→ возвращаетwrapper.
Декоратор со счётчиком вызовов
Здесь классовый декоратор особенно хорош — состояние хранится в атрибуте:
from functools import wraps
class CallCounter:
"""Считает, сколько раз вызвана функция."""
def __init__(self, fn):
wraps(fn)(self) # копируем метаданные функции на экземпляр
self.fn = fn
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.fn(*args, **kwargs)
@CallCounter
def say(message):
print(message)
say("раз")
say("два")
say("три")
print(f"Вызвано {say.count} раз(а)")
Вывод:
раз два три Вызвано 3 раз(а)
Функция-декоратор vs класс-декоратор
| Критерий | Функция-декоратор | Класс-декоратор |
| Синтаксис | замыкание + вложенные def | класс + __init__ + __call__ |
| Хранение состояния | через замыкание | через атрибуты self — явнее |
| Наследование/расширение | неудобно | легко — подклассы |
| Читаемость | коротко для простых случаев | понятнее при сложной логике |
functools.wraps — зачем он нужен
from functools import wraps
class Log:
def __init__(self, fn):
self.fn = fn # БЕЗ wraps
def __call__(self, *args, **kwargs):
print(f"Вызов {self.fn.__name__}")
return self.fn(*args, **kwargs)
@Log
def compute(x):
"""Вычисляет квадрат."""
return x ** 2
print(compute.__name__) # → compute (берётся из self.fn)
print(compute.__doc__) # → None! wraps не применён
Вывод:
compute None
Применяйте @wraps(fn) к обёртке или wraps(fn)(self) в __init__, чтобы сохранить __name__, __doc__ и __module__ оригинальной функции. Это важно для документации, help() и интроспекции.
Коротко
- Класс становится декоратором благодаря методу
__call__. - Декоратор без аргументов:
__init__получает функцию,__call__— вызывает её. - Декоратор с аргументами:
__init__получает аргументы,__call__— функцию, возвращает обёртку. - Используйте
functools.wraps, чтобы сохранить метаданные оригинала. - Классы удобнее функций, когда декоратор хранит состояние (счётчики, кэши, конфиги).
Закрепите практикой
Задачи с автоматической проверкой — решайте прямо здесь, не уходя из учебника.