При создании функций в цикле какое значение они используют при последующем вызове — текущее или сохранённое...

При создании функций в цикле какое значение они используют при последующем вызове — текущее или сохранённое на итерации?

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

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

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

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

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

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

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

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

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

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

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

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

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

def make_handlers(): handlers = [] for number in range(3): def handler(): return number handlers.append(handler) return handlers print([handler() for handler in make_handlers()]) # [2, 2, 2]

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

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

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

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

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

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

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

  1. Чем отличается изменение объекта в замыкании от переназначения переменной?

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

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

  1. Почему аргумент по умолчанию фиксирует значение, но не делает снимок объекта?

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

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

  1. Может ли та же проблема возникнуть внутри декоратора?

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

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