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

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

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

что делает закрытие точно?

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

EDIT: Что касается вашего другого вопроса о том, как преодолеть это, есть два способа, которые приходят на ум:

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

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

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

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

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

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

При импорте добавить от оператора, как предложил Крис Лутц, пример становится:

 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) 

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

 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 

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

 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) 

Вот новый пример, который подчеркивает структуру данных и содержимое закрытия, чтобы помочь прояснить, когда окружающий контекст «сохранен».

 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, а i не находится в закрытии для f_3 (не показано), что предполагает, что объекты закрытия сами по себе являются отдельными объектами.

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

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