Что именно удерживает замыкание после завершения внешней функции?
Замыкание удерживает не копию локальных переменных, а ссылки на специальные ячейки области видимости — cell-объекты, содержащие эти переменные. Поэтому внешняя функция может завершиться, но её состояние останется доступным вложенной функции, пока существует ссылка на замыкание.
Замыкания появились как механизм лексической области видимости: функция должна иметь доступ к переменным того места, где она была создана. Это позволяет возвращать функции с сохранённым состоянием без глобальных переменных и без обязательного создания отдельного класса.
В Python такой подход особенно полезен для фабрик функций, декораторов и обработчиков с конфигурацией. Он отделяет состояние конкретного экземпляра поведения от общего глобального состояния программы.
Локальные переменные внешней функции обычно становятся недоступными после её завершения. Однако возвращённая вложенная функция может продолжать обращаться к ним, что создаёт важный вопрос: где физически находится это состояние и почему оно не уничтожается.
Неверное понимание приводит к ошибкам с изменяемостью данных, поздним связыванием переменных в циклах и неожиданным совместным состоянием нескольких вызовов. В декораторах это может проявиться как общие счётчики, накопление ссылок на крупные объекты или утечки памяти.
Вложенная функция имеет атрибут __closure__, содержащий ячейки замыкания. Каждая ячейка хранит ссылку на объект, связанный с переменной внешней области. Вложенная функция обращается не к локальному стеку завершившегося вызова, а к этим сохранённым ячейкам.
После завершения make_counter переменная count продолжает существовать, потому что next_value ссылается на её ячейку. Ключевое слово nonlocal разрешает изменять значение переменной во внешней, но не глобальной области; без него присваивание создало бы локальную переменную самой next_value.
Замыкание хранит состояние отдельно для каждого вызова фабрики. Поэтому два результата make_counter() имеют разные ячейки и не делят счётчик. При этом изменяемый объект внутри замыкания изменяется через общую ссылку, а не копируется автоматически.
В декораторе замыкание обычно удерживает исходную функцию и параметры конфигурации. Это удобно, но такая ссылка продлевает время жизни удерживаемых объектов. Если состояние сложное, требуется потокобезопасность, сериализация или явный жизненный цикл, callable-объект с методами и атрибутами может быть понятнее и управляемее.
Нужно добавить к функции подсчёт вызовов. Вариант с глобальным словарём прост, но создаёт общее состояние, усложняет тестирование и может конфликтовать между экземплярами функций. Вариант с классом позволяет явно описать состояние и добавить синхронизацию, но требует больше шаблонного кода.
Выбран декоратор с замыканием: счётчик хранится в ячейке, принадлежащей конкретному декорированному экземпляру. Это минимальное решение для однопоточного кода. В многопоточной системе одного замыкания недостаточно: операции изменения счётчика нужно защищать блокировкой либо использовать подходящее потокобезопасное хранилище.
Если декоратор применяется многократно к одной функции, каждое применение обычно создаёт отдельное замыкание. Поэтому важно определить, должен ли счётчик относиться к конкретной обёртке, к исходной функции или ко всему процессу.
Замыкание обычно сохраняет доступ к ячейке, а не независимую копию значения на момент создания функции. Если несколько вложенных функций захватывают одну переменную, они могут читать и изменять одну и ту же ячейку. Поэтому изменение через nonlocal будет видно другим функциям, использующим это замыкание.
Они захватывают переменную цикла, а не её текущее значение на каждой итерации. После завершения цикла все функции обращаются к одной ячейке, в которой осталось последнее значение. Для независимого захвата нужно создать отдельную область видимости на каждой итерации или передать текущее значение как параметр со значением по умолчанию, понимая особенности этого приёма.
Замыкание хорошо подходит для небольшого скрытого состояния и простой операции. Callable-объект предпочтительнее, когда состоянию нужны несколько явно названных полей, методы сброса или диагностики, наследование, настройка жизненного цикла либо синхронизация. Компромисс — больший объём кода, но лучшая расширяемость и наблюдаемость.