/ заметка
Пример интересного замыкания в 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]())
Понятное дело, что в реальной работе такая дичь вряд ли встретится. Но это хороший пример на понимание того, как работает замыкание.