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

недавно я начал играть с 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 to 3. Для каждого из этих чисел a lambda создается функция, которая захватывает i и добавляет его к вход функции. Последняя строка вызывает вторую функции ссылаются на последнее значение i, 3, в результате adders[1](3) возвращение 6.

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

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

6 ответов


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

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

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

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

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

  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 модуль.

при импорте 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)

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

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