Если функция обёртка декоратора содержит yield, что фактически произойдёт при вызове декорированной функции?

Если функция-обёртка декоратора содержит yield, что фактически произойдёт при вызове декорированной функции?

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

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

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

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

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

Генераторы появились как механизм ленивого получения последовательности значений без создания всей последовательности в памяти. Конструкция yield приостанавливает выполнение функции и сохраняет её состояние до следующего шага итерации.

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

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

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

В результате при вызове:

  • исходная функция может ещё не выполняться;
  • обычные побочные эффекты откладываются;
  • исключение возникает не в месте вызова, а во время получения элемента;
  • значение return внутри генератора не становится обычным результатом вызова.

Такой декоратор корректен только тогда, когда ленивое поведение действительно является частью его контракта.

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

Инструкция yield превращает функцию в генераторную функцию. Вызов такой функции создаёт генератор и возвращает управление вызывающему коду; выполнение тела начинается при next() или при обходе в цикле for.

def trace(fn): def wrapper(*args, **kwargs): print('до вызова') yield fn(*args, **kwargs) print('после выдачи') return wrapper @trace def answer(): print('тело функции') return 42 g = answer() # печати из wrapper и answer ещё нет print(next(g)) # выполняется тело до первого yield, выводится 42 next(g) # выполняется остаток wrapper

Вызов answer() возвращает генератор g. При первом next(g) печатается до вызова, затем выполняется answer, её результат выдаётся как единственный элемент. При следующем next(g) выполняется код после yield, после чего генератор завершается.

Если обёртка должна просто вернуть результат исходной функции, в ней нельзя использовать yield; нужен обычный return fn(*args, **kwargs). Если же задача — прозрачно оборачивать поток значений, обычно применяют yield from fn(*args, **kwargs), предполагая, что исходная функция возвращает итерируемый объект.

return внутри генератора завершает его и передаёт значение в StopIteration.value, но не возвращает это значение непосредственно из вызова генераторной функции. Это ещё одно отличие от обычной функции.

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

В сервисе декоратор аудита должен был записывать время начала и окончания обычного вызова обработчика. Разработчик использовал yield, чтобы разместить запись окончания после вызова, но вызов обработчика стал возвращать генератор, а логирование и сам обработчик начали выполняться только после попытки прочитать результат.

Рассматривались два варианта. Обычная обёртка с try/finally сохраняла синхронный контракт и гарантировала выполнение завершающего логирования, но не подходила бы для ленивого потока данных. Генераторная обёртка позволяла обрабатывать поток поэлементно, однако требовала явно документировать итерационный контракт и отдельно учитывать закрытие генератора.

Для обычного обработчика выбрали обычную функцию-обёртку с return и finally. Генераторный вариант оставили только для декоратора, предназначенного именно для функций, возвращающих последовательность: там использовали делегирование итерации и проверили поведение при досрочном прекращении обхода.

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

  1. Когда именно выполняется исходная функция, если в обёртке есть yield?

    Не при вызове декорированной функции, а при продвижении созданного генератора: через next(), цикл for, list() или другой потребитель итератора. Поэтому проверка только факта создания генератора не подтверждает выполнение исходной функции.

  2. Что произойдёт, если генераторная обёртка содержит return fn(*args, **kwargs) вместо yield?

    Вызов всё равно вернёт генератор. При итерации обёртка сразу завершится, а результат fn будет записан в StopIteration.value; обычный for его не выдаст. Чтобы результат стал элементом итерации, нужно использовать yield fn(*args, **kwargs), а для передачи элементов итерируемого результата — yield from fn(*args, **kwargs).

  3. Когда возникнет исключение исходной функции и как это влияет на обработку ошибок?

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