На каком этапе Python применяет декоратор к функции: при её определении или при вызове?

На каком этапе Python применяет декоратор к функции: при её определении или при вызове?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

Синтаксис декораторов был стандартизирован в Python 2.4 в рамках PEP 318. Он решал практическую проблему повторяющегося и плохо читаемого кода, которым функции преобразовывали после определения: например, для регистрации, проверки доступа или измерения времени выполнения.

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

Постановка проблемы

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

Если же дорогостоящая работа должна выполняться при каждом вызове, её нужно помещать в возвращаемую обёртку. Иначе разработчик может случайно выполнить её один раз при импорте или, наоборот, повторять настройку при каждом вызове.

Подробное решение

При выполнении определения функции Python сначала создаёт объект функции. Затем применяет декораторы к нему снизу вверх: результат ближайшего к функции декоратора передаётся следующему. После этого имя функции связывается с окончательным результатом цепочки.

calls = [] def mark(func): calls.append('применён') def wrapper(*args, **kwargs): calls.append('вызван') return func(*args, **kwargs) return wrapper @mark def greet(): return 'Привет' print(calls) # ['применён'] greet() print(calls) # ['применён', 'вызван']

В примере запись @mark эквивалентна логике, при которой результат применения mark становится новым значением имени greet. Поэтому строка применён появляется до вызова greet, а вызван — только внутри обёртки.

Важно различать вычисление выражения декоратора, его применение и вызов результата. Если декоратор параметризован, выражение, создающее декоратор, также вычисляется при выполнении определения функции, после чего полученный декоратор применяется к функции.

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

Для сохранения имени, документации и другой метаинформации обёртки обычно используют functools.wraps. Это не меняет момент применения декоратора, но делает поведение обёрнутой функции удобнее для отладки, документации и инструментов анализа.

Ситуация из практики

В веб-приложении декоратор регистрации маршрута добавлял функцию в таблицу маршрутов. Команда ожидала, что регистрация произойдёт при первом обращении к endpoint, но фактически она выполнялась во время импорта модуля.

Рассматривались два варианта. Перенести регистрацию внутрь обёртки означало бы выполнять её при каждом вызове или добавлять отдельную проверку состояния; это усложняло поток выполнения и создавало риск повторной регистрации. Оставить регистрацию на этапе определения было проще, но требовало учитывать порядок импорта и циклические зависимости.

Выбрали регистрацию при применении декоратора, потому что таблица маршрутов должна быть полностью построена до запуска сервера. При этом тяжёлую инициализацию зависимостей вынесли из декоратора в явный этап запуска приложения. В результате импорт модулей регистрировал метаданные, а внешние ресурсы создавались контролируемо.

Что кандидаты часто упускают

  1. Что именно получает имя функции после применения декоратора?

    Имя обычно связывается не с исходным объектом функции, а с объектом, который вернул декоратор. Чаще всего это функция-обёртка, но декоратор может вернуть вызываемый объект другого типа или даже объект, который нельзя вызвать. Поэтому после декорирования меняются доступные атрибуты, идентичность объекта и иногда его сигнатура.

  2. В каком порядке применяются несколько декораторов?

    Они применяются снизу вверх: ближайший к объявлению функции обрабатывает её первым, а следующий получает уже преобразованный результат. При вызове обёртки выполняются в обратной визуальной последовательности, поскольку внешняя обёртка вызывает внутреннюю. Неверное понимание порядка особенно опасно для декораторов авторизации, кэширования и транзакций.

  3. Что произойдёт, если декоратор завершится исключением?

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