В прошлом уроке была функция make_multiplier, которая создавала и возвращала multiplier. И там оставался открытый вопрос: как multiplier умудряется помнить factor, если make_multiplier уже давно закончила работу и её локальные переменные вроде бы должны были исчезнуть?
Аналогия: представь, что ты уезжаешь в отпуск и оставляешь соседу ключ от квартиры с запиской "полей цветы, пока меня нет". Тебя физически уже нет дома, но сосед всё равно помнит про твою просьбу и про то, где искать цветы. Записка - это как раз то, что удерживает нужную информацию живой даже после того, как хозяин ушёл. Вот это и есть замыкание (closure): внутренняя функция "запоминает" переменные из внешней функции, даже когда внешняя функция уже отработала и вышла.
Вывод:
1
2
3
make_counter() отработала один раз и вернула функцию increment. Но increment продолжает помнить переменную count и продолжает её изменять при каждом вызове. Причём у каждого счётчика своя собственная память:
Вывод:
1
counter2 начинает считать с нуля заново, потому что это отдельный вызов make_counter(), а значит отдельная "записка" с отдельным значением count.
Вот тут обычно и спотыкаются. Смотри, что будет, если убрать nonlocal:
Это упадёт с ошибкой:
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Дело в том, что строчка count += 1 - это на самом деле count = count + 1, то есть присваивание. А как только Python видит присваивание переменной внутри функции, он автоматически считает эту переменную локальной для всей функции целиком, даже если присваивание идёт после чтения. Интерпретатор смотрит на тело increment заранее, видит там count = ... и решает: значит count тут своя, локальная. И когда доходит до count += 1, пытается сначала прочитать локальную count, а её ещё не существует. Отсюда и UnboundLocalError.
Просто читать внешнюю переменную можно без всяких ключевых слов:
Вывод:
x видно снаружи: 10
nonlocal нужен только тогда, когда ты хочешь изменить (переприсвоить) переменную из внешней функции, а не создать свою локальную с таким же именем. nonlocal явно говорит Python: "не создавай новую локальную переменную, работай с той, что уже есть на уровень выше".
Кстати, если тебе нужно менять переменную не из объемлющей функции, а из глобальной области видимости модуля, для этого есть global. Работает по такому же принципу, просто уровень видимости другой: nonlocal смотрит на ближайшую объемлющую функцию, global сразу идёт в область видимости модуля.
Python хранит замкнутые переменные не как копию значения, а как ссылку на общую "ячейку" (cell). Можно даже руками заглянуть внутрь и убедиться:
Вывод:
(<cell at 0x102794700: int object at 0x1032462f8>,)
0
__closure__ это кортеж объектов-ячеек, cell_contents показывает текущее значение внутри ячейки. Именно поэтому nonlocal вообще может работать: он не копирует значение, он меняет содержимое общей ячейки, к которой обращаются и внешняя, и внутренняя функция.
Вот вопрос, который очень любят задавать на собеседовании именно про замыкания: что выведет этот код?
Интуитивно кажется, что должно получиться [0, 1, 2], ведь на каждой итерации i было разным. А на деле:
[2, 2, 2]
Тут работает так называемое late binding, позднее связывание. Замыкание запоминает не значение i в момент создания функции, а саму переменную i целиком, точнее ссылку на её ячейку. А переменная i - одна на весь цикл, и после цикла она равна 2. Когда мы вызываем f() уже после цикла, Python идёт смотреть в ячейку i и находит там финальное значение.
Чинится это через параметр по умолчанию, значение которого действительно фиксируется в момент создания функции, а не читается позже:
Вывод:
[0, 1, 2]
i=i в сигнатуре выглядит непривычно, но означает ровно следующее: "создай параметр с именем i, а значением по умолчанию возьми то, что было в переменной i прямо сейчас, в этот конкретный момент прохода цикла". Значения по умолчанию вычисляются один раз, в момент определения функции, и дальше не меняются - в отличие от переменных внутри тела функции.
nonlocal нужен только для изменения внешней переменной, для чтения он не требуется.UnboundLocalError.def f(i=i).Дальше пойдём к самому декоратору. Мы уже фактически всё для него подготовили: функция как объект, функция высшего порядка, замыкание. Осталось собрать это в одну конструкцию с понятной аналогией из жизни.