Декоратор это функция, которая принимает функцию и возвращает другую функцию (обычно обёртку). Задача «напишите декоратор» есть почти на каждом Python-собесе, от junior до senior.
Синтаксис это сахар
@timer
def handler(): ...
ровно то же самое, что
def handler(): ...
handler = timer(handler)
Декоратор срабатывает один раз при определении функции (обычно при импорте модуля), а обёртка срабатывает при каждом вызове.
Базовый шаблон
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f'{func.__name__}: {time.perf_counter() - start:.3f} c')
return wrapper
Три обязательных детали:
*args, **kwargs, чтобы обёртка принимала любые аргументы.return func(...), иначе декорированная функция начнёт возвращатьNone.functools.wraps, чтобы сохранить__name__,__doc__,__qualname__,__module__и добавить__wrapped__. Без него логи, отладчик, документация и фреймворки (например, роутинг по имени функции) видятwrapper.
Декоратор с параметрами
@retry(times=3) сначала вызывает retry(times=3), и уже результат применяется к функции. Получается три уровня вложенности:
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except ConnectionError:
if attempt == times - 1:
raise
return wrapper
return decorator
Порядок применения
@a
@b
def f(): ...
это f = a(b(f)): ближайший к функции декоратор применяется первым, а при вызове первой срабатывает внешняя обёртка a.
Состояние в декораторе
Обёртка может хранить состояние в замыкании или в атрибуте: wrapper.calls = 0. Это состояние общее для всех вызовов функции, поэтому в многопоточном коде его защищают блокировкой.
Декораторы из стандартной библиотеки
functools.cacheиfunctools.lru_cache(maxsize=128): мемоизация, аргументы должны быть хешируемыми. Уlru_cacheестьcache_info()иcache_clear().@property,@staticmethod,@classmethod: декораторы методов.@dataclass,@functools.total_ordering: декораторы классов, они дописывают методы.contextlib.contextmanager: делает контекстный менеджер из генератора.
Декоратор-класс и декоратор класса
Декоратором может быть любой вызываемый объект, например класс с __call__. А «декоратор класса» принимает класс и возвращает класс: так работает @dataclass.
Частые ошибки
- Забыли
returnрезультата в обёртке. - Нет
functools.wraps. - Перепутали уровни у параметризованного декоратора:
@retryбез скобок передаст функцию вtimes. lru_cacheна методе экземпляра: кеш держит ссылки наselfи не даёт объектам умереть.- Тяжёлая работа в теле декоратора выполняется при импорте.
Что спросят на собесе
- Напишите декоратор, который замеряет время или логирует вызовы.
- Зачем
functools.wraps. - Как сделать декоратор с параметрами и в каком порядке применяются несколько декораторов.
- Когда выполняется код декоратора и когда код обёртки.
- Как работает
lru_cacheи какие у него ограничения.