Декораторы

Материал из llmdocs — справочник разработчика
Портал: Python
Декораторы
Синтаксис@имя над 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]

См. также

Источники

  1. PEP 318 — Decorators for Functions and Methods
  2. PEP 3129 — Class Decorators
  3. Python Glossary — decorator
  4. Python — functools
  5. PEP 612 — Parameter Specification Variables
  6. PEP 614 — Relaxing Grammar Restrictions On Decorators