В фабрике замыканий две внутренние функции изменяют одну переменную: почему вызов одной может изменить результат другой?
Обе функции могут ссылаться на одну и ту же ячейку замыкания, созданную для переменной внешней функции. Поэтому изменение значения через одну функцию становится видимым для другой. Это не копирование значения при создании каждой внутренней функции, а совместный доступ к одному состоянию.
Замыкания появились как способ сохранять лексическое состояние функции после завершения внешнего вызова без глобальных переменных. Такой подход позволяет создавать небольшие stateful-компоненты, не объявляя для каждого из них отдельный класс.
В Python вложенные функции используют переменные из окружающих областей видимости. Если несколько вложенных функций захватывают одну переменную, они получают ссылку на общее окружение, а не независимые копии этой переменной.
Разработчик может ожидать, что каждая возвращённая функция хранит собственное значение. На практике функции, созданные внутри одного вызова фабрики, могут совместно изменять состояние.
Неверное понимание приводит к неожиданным результатам: вызов функции-инкремента меняет значение, которое затем прочитает функция-получатель. При этом разные вызовы самой фабрики обычно создают независимые состояния.
При выполнении внешней функции Python создаёт окружение, в котором хранится переменная. Вложенные функции захватывают не само значение, а доступ к соответствующей ячейке этого окружения. После завершения внешней функции ячейка не уничтожается, пока на неё ссылаются замыкания.
Для изменения переменной из вложенной функции требуется nonlocal. Без него присваивание создало бы локальную переменную внутренней функции, а чтение внешнего значения перед присваиванием могло бы привести к UnboundLocalError.
increment и current получили доступ к одной ячейке count, поэтому второй вызов видит результат первого. Если вызвать make_pair() ещё раз, будет создано новое окружение с отдельной ячейкой; состояние двух пар функций не пересекается.
Такое состояние не является потокобезопасным автоматически. При конкурентных вызовах операция увеличения счётчика может потребовать синхронизации, например блокировки. Кроме того, замыкание скрывает состояние от внешнего кода, поэтому для сложного жизненного цикла, диагностики или сериализации класс часто оказывается понятнее.
Допустим, нужно создать независимые счётчики для нескольких HTTP-клиентов. Глобальный счётчик создаёт конфликт между клиентами, а отдельный класс добавляет явную структуру, но требует больше шаблонного кода.
Фабрика замыканий компактно возвращает операции increment и current, а каждый вызов фабрики изолирует состояние. Это подходящий выбор, если состояние небольшое, интерфейс стабилен и не требуется сериализовать объект.
Если же счётчику нужны сброс, настройка, журналирование и безопасная работа из нескольких потоков, лучше использовать вызываемый или обычный объект с явными атрибутами и синхронизацией. Он немного многословнее, зато состояние и его методы проще исследовать и расширять.
Функции, возвращённые одним вызовом фабрики, захватывают одну ячейку и разделяют состояние. Каждый новый вызов фабрики создаёт новое локальное окружение и новую ячейку, поэтому его функции не видят изменения из другого вызова.
nonlocal, если функция уже её видит?Чтение внешней переменной возможно без nonlocal, потому что Python ищет имя во внешних областях видимости. Но присваивание по умолчанию означает создание локального имени текущей функции. nonlocal явно указывает, что присваивание должно изменить существующее имя из ближайшей внешней функции.
Только для простого и хорошо ограниченного состояния. Замыкание удобно, когда наружу нужно выдать небольшой набор операций и скрыть внутреннюю переменную. Класс предпочтительнее при сложном интерфейсе, наследовании, отладке, сериализации или необходимости явно описать состояние объекта.