Представьте функцию с двумя декораторами: какой декоратор фактически вызывается первым?

Представьте функцию с двумя декораторами: какой декоратор фактически вызывается первым?

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

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

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

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

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

Синтаксис с символом @ делает цепочку преобразований компактной, но скрывает важную деталь: несколько декораторов образуют вложенные вызовы, а не независимый список обработчиков.

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

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

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

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

Для функции f запись декораторов сверху вниз имеет смысл:

@A @B def f(): ...

Это эквивалентно выражению f = A(B(f)). Сначала Python создаёт исходную функцию, затем применяет к ней B, а результат передаёт в A.

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

def first(fn): def wrapper(): print("first") fn() return wrapper def second(fn): def wrapper(): print("second") fn() return wrapper @first @second def task(): print("task") task()

Результат начинается с first, затем выводится second, затем task. Если поменять декораторы местами, изменится и цепочка вызовов.

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

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

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

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

Другой вариант — отказаться от декораторов и написать явный управляющий код. Он лучше показывает сложный жизненный цикл и позволяет точнее обрабатывать ошибки, но увеличивает дублирование и усложняет повторное использование.

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

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

  1. Меняется ли порядок выполнения, если декораторы записаны в обратном порядке?

Да. Для @A над @B итоговая функция имеет вид A(B(f)), а для обратной записи — B(A(f)). Поскольку обёртки могут иметь побочные эффекты и менять результат, это не косметическая перестановка, а изменение поведения программы.

  1. В каком порядке вычисляются аргументы декораторов?

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

  1. Что произойдёт, если внутренний декоратор вернёт объект с другим интерфейсом?

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