Что (лямбда) захватывает функция закрытия? - PullRequest
218 голосов
/ 19 февраля 2010

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

Ответы [ 6 ]

174 голосов
/ 19 февраля 2010

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

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

141 голосов
/ 19 февраля 2010

На ваш второй вопрос ответили, но как и на первый:

что точно захватывает замыкание?

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

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

  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)]
    
29 голосов
/ 11 апреля 2012

Для полноты ответа на второй вопрос: вы можете использовать частичный в модуле 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)
20 голосов
/ 19 февраля 2010

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

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
3 голосов
/ 09 мая 2014

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

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

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

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

>>> print f_1.func_closure is f_2.func_closure
False
3 голосов
/ 19 февраля 2010

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

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