Что делает захват функции лямбда-функции?
Недавно я начал играть с 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