Недавно я начал играть с Python и обнаружил что-то необычное в работе замыканий. Рассмотрим следующий код:
adders=[0,1,2,3]
for i in [0,1,2,3]:
adders[i]=lambda a: i+a
print adders[1](3)
Он создает простой массив функций, которые принимают один вход и возвращают этот вход, добавленный числом. Функции построены в for
цикле, где итератор i
работает от 0
до 3
. Для каждого из этих чисел создается lambda
функция, которая захватывает i
и добавляет ее к входу функции. Последняя строка вызывает вторую lambda
функцию с 3
параметром. К моему удивлению, результат был 6
.
Я ожидал 4
. Я рассуждал так: в Python все является объектом, поэтому каждая переменная является указателем на него. При создании lambda
замыканий для i
я ожидал, что он будет хранить указатель на целочисленный объект, на который в данный момент указывает i
. Это означает, что при i
назначении нового целочисленного объекта это не должно влиять на ранее созданные замыкания. К сожалению, проверка adders
массива в отладчике показывает, что это так. Все lambda
функции относятся к последнему значению i
, 3
, что приводит к adders[1](3)
возвращению 6
.
Что заставляет меня задуматься о следующем:
- Что именно запечатлевают крышки?
- Каков наиболее элегантный способ убедить
lambda
функции захватить текущее значениеi
таким образом, чтобы это не повлияло приi
изменении его значения?
i
покинуть пространство имен?print i
это не сработает после цикла. Но я проверил это для себя, и теперь я понимаю, что вы имеете в виду - это работает. Я понятия не имел, что переменные цикла задерживаются после тела цикла в python.if
,with
, иtry
т.д.Ответы:
На ваш второй вопрос был дан ответ, но что касается вашего первого:
Скоупинг в Python является
динамичным илексическим. Закрытие всегда будет помнить имя и область действия переменной, а не объект, на который она указывает. Поскольку все функции в вашем примере созданы в одной и той же области видимости и используют одно и то же имя переменной, они всегда ссылаются на одну и ту же переменную.РЕДАКТИРОВАТЬ: Относительно вашего другого вопроса о том, как преодолеть это, есть два способа, которые приходят на ум:
Самый краткий, но не совсем эквивалентный способ - тот, который рекомендован Адриеном Плиссоном . Создайте лямбду с дополнительным аргументом и установите значение по умолчанию для дополнительного аргумента для объекта, который вы хотите сохранить.
Немного более многословным, но менее хакерским было бы создавать новую область каждый раз, когда вы создаете лямбду:
Область действия здесь создается с использованием новой функции (лямбда, для краткости), которая связывает свой аргумент и передает значение, которое вы хотите связать в качестве аргумента. В реальном коде, однако, вы скорее всего будете иметь обычную функцию вместо лямбды для создания новой области видимости:
источник
set!
. посмотрите, что такое динамическая область действия: voidspace.org.uk/python/articles/code_blocks.shtml .Вы можете принудительно захватить переменную, используя аргумент со значением по умолчанию:
идея состоит в том, чтобы объявить параметр (с умным именем
i
) и присвоить ему значение по умолчанию для переменной, которую вы хотите захватить (значениеi
)источник
Для полноты еще один ответ на ваш второй вопрос: вы можете использовать частичное в модуле functools .
С предложением Криса Лутца, импортирующим оператор add, следующий пример:
источник
Рассмотрим следующий код:
Я думаю, что большинство людей не будет смущать это вообще. Это ожидаемое поведение.
Итак, почему люди думают, что все будет по-другому, когда это будет сделано в цикле? Я знаю, что сделал эту ошибку сам, но я не знаю почему. Это петля? Или, может быть, лямбда?
В конце концов, цикл - это просто более короткая версия:
источник
i
одна и та же переменная доступна для каждой лямбда-функции.В ответ на ваш второй вопрос, самый элегантный способ сделать это - использовать функцию, которая принимает два параметра вместо массива:
Однако использовать лямбду здесь немного глупо. Python предоставляет нам
operator
модуль, который предоставляет функциональный интерфейс для основных операторов. Лямбда, приведенная выше, имеет ненужные издержки только для вызова оператора сложения:Я понимаю, что вы играете вокруг, пытаясь изучить язык, но я не могу представить себе ситуацию, я бы использовал массив функций, где мешала бы странная область видимости Python.
Если вы хотите, вы можете написать небольшой класс, который использует ваш синтаксис индексации массива:
источник
Вот новый пример, который подчеркивает структуру данных и содержимое замыкания, чтобы помочь уточнить, когда включающий контекст «сохраняется».
Что находится в закрытии?
Примечательно, что my_str не находится в закрытии f1.
Что в закрытии f2?
Обратите внимание (из адресов памяти), что оба замыкания содержат одни и те же объекты. Итак, вы можете начать думать о лямбда-функции как о ссылке на область видимости. Однако my_str не находится в замыкании для f_1 или f_2, а i не находится в замыкании для f_3 (не показано), что предполагает, что сами объекты замыкания являются отдельными объектами.
Являются ли сами объекты замыкания одним и тем же объектом?
источник
int object at [address X]>
заставил меня думать, что закрытие хранит [адрес X] АКА ссылку. Однако [адрес X] изменится, если переменная будет переназначена после лямбда-оператора.