Вопрос-Ответ

What do lambda function closures capture?

Что фиксируют замыкания лямбда-функции?

Недавно я начал играть с 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.

Если вы попали сюда, пытаясь решить проблему с созданием кнопок в Tkinter, попробуйте tkinter создает кнопки в цикле for, передавая аргументы команды для получения более конкретного совета.

Смотрите, что именно содержится в obj.__closure__? технические подробности о том, как Python реализует замыкания. Смотрите В чем разница между ранним и поздним связыванием? для обсуждения соответствующей терминологии.

Переведено автоматически
Ответ 1

вы можете принудительно фиксировать переменную, используя аргумент со значением по умолчанию:

>>> 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:


  1. 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.



  2. 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:


    def createAdder(x):
    return lambda y: y + x
    adders = [createAdder(i) for i in range(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"

def print_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
2024-01-29 14:08 python