Программирование PythonФункции и декораторыPython-разработчик серверной части

Разберите механизм: почему рекурсивная функция после декорирования обычно проходит через обёртку на каждом ...

Разберите механизм: почему рекурсивная функция после декорирования обычно проходит через обёртку на каждом рекурсивном вызове?

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

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

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

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

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

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

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

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

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

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

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

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

Минимальный пример:

def trace(fn): def wrapper(n): print("вход", n) return fn(n) return wrapper @trace def factorial(n): if n < 2: return 1 return n * factorial(n - 1) factorial(3)

Здесь первый вызов находит wrapper. Обёртка вызывает исходную factorial через fn, но внутри исходной функции выражение factorial(n - 1) снова находит имя factorial, связанное с wrapper. Поэтому обёртка срабатывает для значений 3, 2 и 1.

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

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

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

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

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

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

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

  1. Всегда ли рекурсивный вызов проходит через декоратор?

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

  1. Почему вызов исходной функции через fn внутри обёртки не предотвращает повторную обёртку?

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

  1. Как декорировать рекурсивную функцию только один раз на внешний вызов?

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