Урок курса

Декораторы в Python: функции, @ и параметры

Python для профессионалов

Декоратор принимает вызываемый объект и возвращает другой вызываемый объект. Это позволяет добавить логирование, проверку прав, кэширование или измерение времени, не смешивая служебную логику с телом функции.

from functools import wraps

def traced(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"call {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@traced
def add(a: int, b: int) -> int:
    return a + b

Запись @traced эквивалентна add = traced(add). Декоратор выполняется в момент определения функции, а wrapper — при каждом вызове.

Почему нужен functools.wraps

Без @wraps(func) наружу торчит имя и документация обёртки. Это мешает логам, introspection, генерации документации и некоторым фреймворкам. wraps копирует важные метаданные и сохраняет ссылку __wrapped__ на исходную функцию.

Обёртка почти всегда принимает *args, **kwargs, чтобы не ломать разные сигнатуры. При этом типизацию сложных универсальных декораторов лучше оформлять через ParamSpec и TypeVar; простая аннотация Callable[..., Any] скрывает ошибки аргументов.

Порядок нескольких декораторов важен. В записи @a над @b сначала создаётся b(func), затем a(...). При вызове внешняя обёртка a срабатывает первой.

Декоратор с параметрами

Если у декоратора есть настройка, появляется ещё один уровень функции — фабрика декораторов.

from functools import wraps

def repeat(times: int):
    if times < 1:
        raise ValueError("times must be positive")

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

Теперь @repeat(3) сначала вызывает фабрику, получает декоратор и только затем оборачивает функцию. Не прячьте в декоратор критичное поведение без явного имени: транзакции, retry и авторизация должны оставаться наблюдаемыми и тестируемыми.

Попробуйте решить

Зачем обычно добавляют @functools.wraps к wrapper-функции?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку