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

Объясните механизм: как замыкание сохраняет изменяемое состояние между вызовами внутренней функции через no...

Объясните механизм: как замыкание сохраняет изменяемое состояние между вызовами внутренней функции через nonlocal?

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

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

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

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

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

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

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

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

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

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

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

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

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

def make_counter(): value = 0 def count(): nonlocal value value += 1 return value return count first = make_counter() second = make_counter() print(first()) # 1 print(first()) # 2 print(second()) # 1

У first и second разные ячейки value, потому что они созданы разными вызовами make_counter. Повторный вызов first обращается к той же ячейке и изменяет сохранённое значение.

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

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

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

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

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

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

  1. Чем nonlocal отличается от global?

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

  1. Почему изменение списка возможно без nonlocal, а переназначение списка требует nonlocal?

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

  1. Что произойдёт, если две внутренние функции возвращены одним вызовом внешней функции?

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