Декораторы и «магия»
почему код выполняется не сверху вниз
Что это
Декоратор — строка со знаком @ над функцией. Она означает: эту функцию
оборачивают в другую, и при вызове сначала выполнится обёртка.
Знать это нужно по простой причине: из-за декораторов код выполняется не в том порядке, в котором написан. Для того, кто читает код, это главный источник недоумения.
Как выглядит в коде
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регистрируют функцию: её вызовет фреймворк.Функция нигде не вызывается, но работает — ищи декоратор над ней.
Несколько декораторов применяются снизу вверх.