Декораторы вглубь
Разбираем декораторы с аргументами, классы-декораторы и помощники из functools.
Декоратор — функция (или класс), которая принимает другую функцию и возвращает новую, обычно расширяя поведение исходной без изменения её кода.
Декоратор с аргументами
Обычный декоратор — функция от функции. Чтобы декоратор принимал свои параметры (например, «повтори N раз»), нужен ещё один уровень: внешняя функция принимает аргумент и возвращает сам декоратор.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Привет, {name}!")
return name
greet("Аня")
print("Имя функции:", greet.__name__)
Вывод:
Привет, Аня! Привет, Аня! Привет, Аня! Имя функции: greet
Зачем functools.wraps
Без @functools.wraps(func) обёртка «съедает» метаданные исходной функции: greet.__name__ стал бы "wrapper", а docstring исчез бы. wraps копирует имя, документацию и другие атрибуты с оригинала на обёртку — поэтому в выводе мы видим greet, а не wrapper. Всегда используйте его в декораторах.
Класс-декоратор
Декоратором может быть и класс — если у него есть метод __call__. Это удобно, когда декоратору нужно хранить состояние между вызовами, например счётчик.
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Вызов №{self.count} функции {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hi():
print("Привет!")
say_hi()
say_hi()
print("Всего вызовов:", say_hi.count)
Вывод:
Вызов №1 функции say_hi Привет! Вызов №2 функции say_hi Привет! Всего вызовов: 2
Состояние (self.count) живёт в экземпляре класса — после декорирования say_hi это объект CountCalls, поэтому у него есть атрибут count.
lru_cache: мемоизация из коробки
functools.lru_cache — готовый декоратор, кеширующий результаты функции по её аргументам. Повторный вызов с теми же аргументами возвращает сохранённое значение мгновенно, без пересчёта.
import functools
calls = {"n": 0}
@functools.lru_cache(maxsize=None)
def slow_square(x):
calls["n"] += 1 # считаем реальные вычисления
return x * x
print(slow_square(4))
print(slow_square(4)) # из кеша, без вычисления
print(slow_square(5))
print("Реальных вычислений:", calls["n"])
print(slow_square.cache_info())
Вывод:
16 16 25 Реальных вычислений: 2 CacheInfo(hits=1, misses=2, maxsize=None, currsize=2)
Заметьте: slow_square(4) вызван дважды, но реальных вычислений всего два (на 4 и на 5) — второй запрос на 4 взят из кеша. Это незаменимо для дорогих чистых функций, например рекурсивных (числа Фибоначчи).
Итог
- Декоратор с аргументами — это три уровня функций: внешняя берёт параметры и возвращает декоратор.
functools.wrapsсохраняет имя и docstring исходной функции на обёртке.- Класс-декоратор (через
__call__) удобен для хранения состояния между вызовами. lru_cacheкеширует результаты по аргументам — мгновенная мемоизация.