/ заметка

Пример интересного замыкания в Python

Вот такой интересный пример мне попался:

Вот такой интересный пример мне попался:

abc =[]
for i in range(10):
    abc.append(lambda : i)

print(abc[5]())

Как вы думаете, что выведет этот код? я тоже думал, что 5, однако, он выведет 9. Чтобы немного “разгрузить” конструкцию, вытащим лямбду в именованную функцию:

abc =[]
for i in range(10):
	def f():
	    return i
    abc.append(f)

print(abc[5]())

Это тот же самый пример, просто мы заменили анонимную функцию именованной. Итак, у нас создается в цикле список из 10 функций. Каждая из которых возвращает i. И кажется, что на месте abc[5] находится функция, которая возвращает 5, достаточно её вызвать. Но результат другой, выводится 9. Более того, если мы выполним любую из функций в этом списке, всегда будет возвращаться 9. И тут уже можно догадаться, что возвращается всегда число, на которое ссылалось имя i, которое было последним в цикле.

Дело в том, что это поведение - тоже замыкание. [[Замыкания в Python|Ранее]] я говорил, что замыкания создаются в enclosing scope. Однако, это не совсем верно. Замыкания создаются всегда, когда переменная захватывается из внешней (по отношению к функции) области видимости. Однако, если у нас нет enclosing scope, нет циклов, то это ничем себя не проявляет. Единственное, что мы не можем менять эту переменную (без явного указания инструкцией global). Фишка enclosing scope и циклов в том, что внутри этих конструкций мы можем менять значения переменных (то есть, в парадигме Python, менять то, куда ссылается имя переменной). И поэтому появляются вот такие забавные эффекты.

Итак, еще раз. Мы проходимся циклом, создаются функции и записываются в список. Функции разные, но! они захватывают имя переменной i из внешней области, не выполняя её (то есть, не подставляя значение, на которое ссылается i), а запоминая само имя переменной (i). Таким образом, каждая итерация в цикле записывает для всех создаваемых функций своё значение возвращаемого i. В конце цикла i становится равным 9. И после выхода из цикла имя i будет ссылаться на значение 9. И поэтому вызов любой функции в созданном списке будет возвращать 9.

Как же сделать так, чтобы такого не произошло? Чтобы вызов abc[5]() возвращал нам 5. Для этого нужно убрать замыкание. Нужно создать в локальной области видимости переменную и присвоить ей значение i. Например, так:

abc =[]
for i in range(10):
    def f(x=i):
        return x
    abc.append(f)

print(abc[5]())

Можно даже вместо x использовать такое же имя i - оно будет создано в локальной области и не будет конфликтовать с внешней i. Ну и заодно вернём лямбду:

abc =[]
for i in range(10):
    abc.append(lambda i=i: i)

print(abc[5]())

Понятное дело, что в реальной работе такая дичь вряд ли встретится. Но это хороший пример на понимание того, как работает замыкание.