В фабрике замыканий две внутренние функции изменяют одну переменную: почему вызов одной может изменить резу...

В фабрике замыканий две внутренние функции изменяют одну переменную: почему вызов одной может изменить результат другой?

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

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

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

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

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

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

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

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

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

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

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

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

def make_pair(): count = 0 def increment(): nonlocal count count += 1 return count def current(): return count return increment, current increment, current = make_pair() print(increment()) # 1 print(current()) # 1

increment и current получили доступ к одной ячейке count, поэтому второй вызов видит результат первого. Если вызвать make_pair() ещё раз, будет создано новое окружение с отдельной ячейкой; состояние двух пар функций не пересекается.

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

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

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

Фабрика замыканий компактно возвращает операции increment и current, а каждый вызов фабрики изолирует состояние. Это подходящий выбор, если состояние небольшое, интерфейс стабилен и не требуется сериализовать объект.

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

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

  1. Чем отличается общее состояние функций от общего состояния разных вызовов фабрики?

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

  1. Почему для изменения захваченной переменной нужен nonlocal, если функция уже её видит?

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

  1. Можно ли считать замыкание полноценной заменой класса для хранения состояния?

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