Программирование с ИИ
ENRU
2.9

Декораторы и «магия»

почему код выполняется не сверху вниз

Что это

Декоратор — строка со знаком @ над функцией. Она означает: эту функцию оборачивают в другую, и при вызове сначала выполнится обёртка.

Знать это нужно по простой причине: из-за декораторов код выполняется не в том порядке, в котором написан. Для того, кто читает код, это главный источник недоумения.

Как выглядит в коде

import functools
import time


def timed(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        t = time.perf_counter()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__}: {time.perf_counter() - t:.3f} c")
        return result
    return wrapper


@timed
def slow_sum(n: int) -> int:
    return sum(range(n))


print(slow_sum(1_000_000))

Вывод:

измерено
slow_sum: 0.016 c
499999500000

Сообщение о времени напечаталось раньше результата, хотя в коде print стоит после. Так работает обёртка: вызов slow_sum(...) на самом деле вызывает wrapper, тот запускает настоящую функцию, замеряет время, печатает и только потом возвращает результат.

Как читать

@имя над функцией — функцию обернули. Чтобы понять, что произойдёт при вызове, надо посмотреть на имя, а не только на саму функцию.

*args, **kwargs — «любые аргументы». Обёртка не знает, что принимает исходная функция, поэтому принимает всё и передаёт дальше.

functools.wraps — служебная деталь: сохраняет у обёртки имя и описание исходной функции. Без неё slow_sum.__name__ вернул бы wrapper, и трейсбеки стали бы непонятными.

Декораторы, которые встретятся чаще всего

ДекораторЧто делает
@router.message(...) (aiogram)«вызывай эту функцию на такое сообщение»
@app.get("/path") (FastAPI)«вызывай на такой HTTP-запрос»
@dataclassсоздаёт __init__ и вывод за тебя
@propertyметод, который читается как поле, без скобок
@staticmethodметод, которому не нужен self
@functools.cacheзапоминает результат, при том же аргументе не считает заново

Первые два — самые важные для практики. Они регистрируют функцию: ты её нигде не вызываешь, но она вызовется, когда придёт нужное событие.

@router.message(Command("weather"))
async def cmd_weather(message: Message) -> None:
    ...

Поиском по проекту вызов cmd_weather не найдётся. Это нормально: её вызывает фреймворк, а строка с @ — заявка на это.

Другие языки

То же самое есть везде, под другими именами:

// JavaScript: обёртка явная, без синтаксиса @
const timedSum = withTiming(slowSum);
// Dart: аннотации @override, @immutable — чаще подсказки,
// чем обёртки

В React похожую роль играют хуки: useEffect не вызывается тобой напрямую — React вызывает его сам в нужный момент.

Как сказать ИИ

Запрос
Что делает декоратор @timed с функцией slow_sum?
Объясни, в каком порядке выполнятся строки при вызове.

Когда непонятно, кто вызывает функцию:

Запрос
Эта функция нигде не вызывается в коде. Кто её вызовет
и когда?

На что смотреть

Порядок декораторов. Если их несколько, они применяются снизу вверх — ближайший к функции первым. Перестановка меняет поведение.

Декоратор молча глотает ошибки. Обёртка с try/except внутри — ошибки исходной функции могут не дойти до тебя (статья 2.8).

Обёртка меняет аргументы или результат. Прочитай, что именно передаётся в fn(...) и что возвращается: обёртка может подменить и то и другое.

Нет functools.wraps. Мелочь, но из-за неё в трейсбеках вместо имён функций будет wrapper, и отладка станет труднее.

Шпаргалка

  • @имя над функцией — её обернули, смотри на обёртку.

  • Из-за декораторов код выполняется не сверху вниз.

  • *args, **kwargs — «принимаю что угодно и передаю дальше».

  • @router.message, @app.get регистрируют функцию: её вызовет фреймворк.

  • Функция нигде не вызывается, но работает — ищи декоратор над ней.

  • Несколько декораторов применяются снизу вверх.