Недавно я начал играть с Python и обнаружил нечто необычное в том, как работают замыкания. Рассмотрим следующий код:
adders=[None, None, None, None]
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 изменение его значения?
Для получения более доступной и практичной версии вопроса, специфичной для случая, когда используется цикл (или понимание списка, выражения генератора и т.д.), см. Создание функций (или лямбда-выражений) в цикле (или понимании). Этот вопрос направлен на понимание базового поведения кода в Python.
вы можете принудительно фиксировать переменную, используя аргумент со значением по умолчанию:
>>> for i in [0,1,2,3]: ... adders[i]=lambda a,i=i: i+a # note the dummy parameter with a default value ... >>> print( adders[1](3) ) 4
идея состоит в том, чтобы объявить параметр (с умным именем i) и присвоить ему значение по умолчанию переменной, которую вы хотите зафиксировать (значение i)
Ответ 2
Что именно фиксируют замыкания?
Closures in Python use lexical scoping: they remember the name and scope of the closed-over variable where it is created. However, they are still late binding: the name is looked up when the code in the closure is used, not when the closure is created. Since all the functions in your example are created in the same scope and use the same variable name, they always refer to the same variable.
There are at least two ways to get early binding instead:
The most concise, but not strictly equivalent way is the one recommended by Adrien Plisson. Create a lambda with an extra argument, and set the extra argument's default value to the object you want preserved.
More verbosely but also more robustly, we can create a new scope for each created lambda:
>>> adders = [0,1,2,3] >>> for i in [0,1,2,3]: ... adders[i] = (lambda b: lambda a: b + a)(i) ... >>> adders[1](3) 4 >>> adders[2](3) 5
The scope here is created using a new function (another lambda, for brevity), which binds its argument, and passing the value you want to bind as the argument. In real code, though, you most likely will have an ordinary function instead of the lambda to create the new scope:
defcreateAdder(x): returnlambda y: y + x adders = [createAdder(i) for i inrange(4)]
Ответ 3
For completeness another answer to your second question: You could use partial in the functools module.
With importing add from operator as Chris Lutz proposed the example becomes:
from functools import partial from operator import add # add(a, b) -- Same as a + b.
adders = [0,1,2,3] for i in [0,1,2,3]: # store callable object with first argument given as (current) i adders[i] = partial(add, i)
print adders[1](3)
Ответ 4
Consider the following code:
x = "foo"
defprint_x(): print x
x = "bar"
print_x() # Outputs "bar"
I think most people won't find this confusing at all. It is the expected behaviour.
So, why do people think it would be different when it is done in a loop? I know I did that mistake myself, but I don't know why. It is the loop? Or perhaps the lambda?
After all, the loop is just a shorter version of:
adders= [0,1,2,3] i = 0 adders[i] = lambda a: i+a i = 1 adders[i] = lambda a: i+a i = 2 adders[i] = lambda a: i+a i = 3 adders[i] = lambda a: i+a