Декораторы
| Синтаксис | @имя над def или class |
|---|---|
| Введены | PEP 318 (Python 2.4, 2004) |
| Декораторы классов | PEP 3129 (Python 3.0) |
| Произвольные выражения | PEP 614 (Python 3.9) |
| Типизация | ParamSpec (PEP 612) |
| Встроенные | property, staticmethod, classmethod, functools.cache, dataclass |
Декоратор в Python — функция, которая принимает другую функцию (или класс) и возвращает её изменённую или обёрнутую версию. Синтаксис @decorator над определением функции — сокращение для присваивания func = decorator(func). Декораторы позволяют добавлять общее поведение — кэширование, логирование, замер времени, проверку прав, повторные попытки, регистрацию обработчиков — не трогая код самой функции.
Декораторы повсюду в экосистеме Python: маршруты в FastAPI и Flask (@app.get("/")), фикстуры pytest (@pytest.fixture), @dataclass, @property.
История
Обёртывание функций было возможно всегда, но выглядело громоздко: method = staticmethod(method) после определения. PEP 318 ввёл синтаксис с символом @ в Python 2.4 (2004);[1] выбор символа долго обсуждали, рассматривая десятки вариантов. В Python 3.0 декораторы распространили на классы (PEP 3129),[2] а с Python 3.9 после @ можно писать любое выражение (PEP 614).[6]
Как работает
Функции в Python — обычные объекты: их можно передавать как аргументы и возвращать из других функций. Декоратор пользуется этим и замыканием — вложенная функция запоминает переменные внешней:
import functools, time
def timed(func):
@functools.wraps(func) # сохранить имя, docstring и сигнатуру оригинала
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.3f} с")
return wrapper
@timed # то же, что load = timed(load)
def load(path):
...
functools.wraps копирует в обёртку имя, документацию и другие атрибуты исходной функции; без него отладка, трассировки и документация показывали бы wrapper вместо настоящего имени.[4]
Декораторы с параметрами
Если декоратору нужны аргументы, добавляется ещё один уровень: внешняя функция принимает параметры и возвращает сам декоратор.
def retry(times: int = 3, delay: float = 1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times:
raise
time.sleep(delay * 2 ** (attempt - 1)) # экспоненциальная пауза
return wrapper
return decorator
@retry(times=5, exceptions=(ConnectionError, TimeoutError))
def fetch(url): ...
Несколько декораторов применяются снизу вверх: в записи @a над @b над def f получается f = a(b(f)).
Встроенные и стандартные декораторы
| Декоратор | Назначение |
|---|---|
@property |
метод, читаемый как атрибут; с @x.setter — управляемая запись |
@staticmethod, @classmethod |
методы без экземпляра; с классом вместо экземпляра |
@functools.cache, @functools.lru_cache(maxsize=…) |
запоминание результатов (мемоизация), см. Кэш (вычисления) |
@functools.cached_property |
свойство, вычисляемое один раз на экземпляр |
@functools.singledispatch |
разные реализации функции в зависимости от типа аргумента |
@dataclasses.dataclass |
генерация __init__, __repr__, __eq__ по полям (см. Dataclasses и Pydantic) |
@contextlib.contextmanager |
генератор как контекстный менеджер |
@typing.override, @typing.final |
пометки для анализаторов типов |
from functools import cache
@cache
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(200) # мгновенно: каждое значение вычисляется один раз
Декораторы классов и регистрация
Декоратор может не оборачивать функцию, а лишь зарегистрировать её и вернуть без изменений — так фреймворки собирают маршруты, команды CLI и обработчики событий:
HANDLERS = {}
def on(event: str):
def register(func):
HANDLERS[event] = func
return func
return register
@on("user.created")
def send_welcome(user): ...
Декораторы классов получают класс целиком и могут добавлять методы или проверять его определение — как @dataclass.
Типизация декораторов
Обёртка с *args, **kwargs скрывает сигнатуру исходной функции от анализаторов типов. ParamSpec (PEP 612) позволяет сохранить её:[5]
from collections.abc import Callable
def logged[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("вызов", func.__name__)
return func(*args, **kwargs)
return wrapper
Теперь mypy и pyright проверяют аргументы при вызове обёрнутой функции так же, как исходной (см. Типизация в Python (type hints, PEP 484)).
Советы
- Всегда используйте
functools.wrapsв обёртках. - Не прячьте в декораторах важную бизнес-логику: поведение, неочевидное из вызова, затрудняет чтение кода.
- Обёртка добавляет накладные расходы на вызов — для горячих функций в тесных циклах это заметно.
- Для асинхронных функций обёртка тоже должна быть
async defи использоватьawait.
Определение и примеры — в глоссарии Python.[3]