Разберите, почему функции, созданные внутри цикла, при последующем вызове используют одно и то же последнее значение переменной цикла.
Функция, созданная внутри цикла, обычно не сохраняет текущее значение переменной цикла, а замыкает саму переменную. При вызове функция обращается к актуальному значению этой переменной, поэтому после завершения цикла все такие функции видят последнее значение.
Чтобы зафиксировать значение на каждой итерации, его нужно явно сохранить: например, передать как значение аргумента по умолчанию или создать отдельную область видимости.
Замыкания и лексические области видимости нужны для того, чтобы вложенные функции могли обращаться к переменным внешней функции после её завершения. Такой подход позволяет сохранять состояние без глобальных переменных и широко используется в обработчиках событий, фабриках функций и декораторах.
При этом Python сохраняет связь с переменной, а не автоматически делает снимок её значения в момент создания функции. Это согласуется с общей моделью разрешения имён, но создаёт известную ловушку в циклах.
Предположим, программа создаёт набор обработчиков для элементов списка, а затем вызывает их значительно позже. Если обработчики используют переменную цикла из внешней области, к моменту вызова цикл уже завершён.
Неверное понимание механизма приводит к тому, что все обработчики работают так, будто были созданы для одного последнего элемента. Ошибка особенно неприятна в асинхронном коде и GUI-приложениях: место создания обработчика выглядит корректно, а проявляется проблема только при фактическом событии.
При создании вложенной функции Python формирует замыкание. Для переменной из внешней функции создаётся объект-контейнер, часто называемый ячейкой; функция хранит ссылку на эту ячейку, а не копию текущего объекта.
Все функции обращаются к одной переменной item, поэтому результатом будет список из трёх значений c. Важно отличать это от изменения самого объекта: здесь меняется привязанное к имени значение, а функции читают его позднее.
Один из способов исправления — сохранить значение через аргумент по умолчанию: lambda item=item: item. Тогда текущее значение вычисляется при создании функции и помещается в локальное пространство её параметров.
Другой вариант — отдельная фабрика функций, создающая новую область видимости для каждой итерации. Для передачи аргументов также можно использовать functools.partial; это обычно яснее, если функция уже существует и требуется зафиксировать часть её параметров.
Аргумент по умолчанию удобен, но может скрывать намерение и не подходит, если значение должно оставаться динамическим. Фабрика лучше выражает семантику замыкания, однако создаёт дополнительную функцию; partial хорошо показывает привязку параметров, но применим прежде всего к вызываемым объектам с подходящей сигнатурой.
В сервисе регистрации обработчиков для нескольких очередей каждый обработчик должен был передавать имя своей очереди. Разработчик создал обработчики в цикле и обнаружил, что сообщения из любой очереди направляются в последнюю очередь списка.
Рассматривались три решения:
Выбрали фабрику, потому что обработчики были частью долгоживущей инфраструктуры, а явная передача контекста упростила тестирование и дальнейшее изменение логики. После исправления каждый обработчик стал обращаться только к своей очереди, а тесты смогли проверять имя очереди как обычный параметр.
Да, ловушка сохраняется. Вложенная функция будет замыкать локальную переменную функции, но все созданные в одном цикле обработчики обычно всё равно будут ссылаться на одну и ту же ячейку. Завершение внешней функции не уничтожает эту переменную, пока на неё ссылается замыкание.
Значения аргументов по умолчанию вычисляются один раз при создании объекта функции. Поэтому выражение item=item сохраняет тогдашний объект в локальном параметре конкретной функции, тогда как обращение к свободной переменной выполняется при вызове через замкнутую ячейку.
Это не означает, что объект становится неизменяемым: если сохранённый объект изменяем, его содержимое всё ещё можно изменить. Фиксируется ссылка на объект, а не глубокая копия.
При позднем связывании меняется значение, на которое указывает имя, и все функции читают последнее связывание. При проблеме с изменяемым объектом имя может оставаться привязанным к тому же объекту, но изменяется сам объект, поэтому одинаковый результат может быть вызван другой причиной.
Для диагностики нужно проверить, какие именно имена являются свободными переменными замыкания, и отдельно установить, менялось ли содержимое сохранённых объектов. Исправление через копирование объекта допустимо только при требуемой семантике снимка; иначе оно добавляет лишние расходы и может нарушить ожидаемую совместную работу с объектом.