Что делает захват функции лямбда-функции?

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

Я ожидал a 4. Мои рассуждения были: в Python все является объектом, и поэтому каждая переменная имеет важное значение для указателя на него. При создании закрытий lambda для i я ожидал, что он сохранит указатель на целочисленный объект, на который в данный момент указывает i. Это означает, что когда i назначается новый целочисленный объект, он не должен влиять на ранее созданные закрытия. К сожалению, проверка массива adders в отладчике показывает, что это так. Все функции lambda относятся к последнему значению i, 3, что приводит к возврату adders[1](3) 6.

Что заставляет меня задуматься о следующем:

  • Что точно фиксируют замыкания?
  • Каков самый элегантный способ убедить функции lambda для захвата текущего значения i таким образом, чтобы это не повлияло, когда i изменит его значение?

Ответ 1

Ваш второй вопрос был дан ответ, но что касается вашего первого:

что делает захват точно?

Scoping в Python - это dynamic и лексический. Закрытие всегда будет помнить имя и область действия переменной, а не объект, на который она указывает. Поскольку все функции в вашем примере создаются в той же области действия и используют одно и то же имя переменной, они всегда ссылаются на одну и ту же переменную.

РЕДАКТИРОВАТЬ: Что касается вашего другого вопроса о том, как преодолеть это, на ум приходят два способа:

  • Самый краткий, но не совсем эквивалентный способ - это рекомендованный Адриеном Плиссоном. Создайте лямбда с дополнительным аргументом и установите дополнительное значение по умолчанию для объекта, который вы хотите сохранить.

  • Немного более подробный, но менее хаккий будет создавать новую область каждый раз, когда вы создаете лямбда:

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

    Область здесь создается с использованием новой функции (для краткости лямбда), которая связывает ее аргумент и передает значение, которое вы хотите связать в качестве аргумента. Однако в реальном коде вы, скорее всего, будете иметь обычную функцию вместо лямбда для создания новой области:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]
    

Ответ 2

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

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

Ответ 3

Для полноты другого ответа на ваш второй вопрос: вы можете использовать partial в functools.

При импорте add от оператора, как предложил Крис Лутц, пример будет выглядеть следующим образом:

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

Рассмотрим следующий код:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Я думаю, что большинство людей не найдут это запутанным вообще. Это ожидаемое поведение.

Итак, почему люди думают, что это будет иначе, когда это будет сделано в цикле? Я знаю, что сама совершила эту ошибку, но я не знаю почему. Это петля? Или, может быть, лямбда?

В конце концов, цикл представляет собой только более короткую версию:

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

Ответ 5

В ответ на ваш второй вопрос самым изящным способом сделать это будет использование функции, которая принимает два параметра вместо массива:

add = lambda a, b: a + b
add(1, 3)

Однако использование лямбда здесь немного глупо. Python предоставляет нам модуль operator, который обеспечивает функциональный интерфейс для основных операторов. Лямбда выше имеет лишние накладные расходы только для вызова оператора сложения:

from operator import add
add(1, 3)

Я понимаю, что вы играете, пытаясь исследовать язык, но я не могу представить себе ситуацию, в которой я бы использовал массив функций, в которых странная видимость Python мешала бы.

Если вы хотите, вы можете написать небольшой класс, который использует синтаксис индекса массива:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

Ответ 6

Здесь приведен новый пример, который подчеркивает структуру данных и содержимое закрытия, чтобы помочь прояснить, когда закрытый контекст "сохранен".

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

Что находится в закрытии?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

Примечательно, что my_str не находится в закрытии f1.

Что в закрытии f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Обратите внимание (из адресов памяти), что оба закрытия содержат одни и те же объекты. Итак, вы можете начать думать о лямбда-функции как о ссылке на область действия. Однако my_str не находится в замыкании для f_1 или f_2, и я не находится в замыкании для f_3 (не показано), что предполагает, что объекты замыкания сами по себе являются отдельными объектами.

Являются ли объекты замыкания самим одним и тем же объектом?

>>> print f_1.func_closure is f_2.func_closure
False