Подтвердить что ты не робот

Почему операторы вывода Python образуют закрытие?

У меня есть две функции, которые возвращают список функций. Функции принимают число x и добавляют к нему i. i - целое число, возрастающее от 0-9.

def test_without_closure():
    return [lambda x: x+i for i in range(10)]



def test_with_yield():
    for i in range(10):
        yield lambda x: x+i

Я бы ожидал, что test_without_closure вернет список из 10 функций, каждый из которых добавит 9 в x, так как значение i равно 9.

print sum(t(1) for t in test_without_closure()) # prints 100

Я ожидал, что test_with_yield также будет иметь такое же поведение, но он правильно создает 10 функций.

print sum(t(1) for t in test_with_yield()) # print 55

Мой вопрос заключается в том, дает ли форму в закрытии в Python?

4b9b3361

Ответ 1

Утилизация не создает закрытие в Python, lambdas создает закрытие. Причина, по которой вы получаете все 9s в "test_without_closure", заключается не в том, что закрытие не происходит. Если этого не произошло, вы не сможете получить доступ к i вообще. Проблема в том, что все замыкания содержат ссылку¹ той же переменной i, которая в конце функции будет равна 9.

В test_with_yield эта ситуация не сильно отличается. Почему же вы получаете разные результаты? Поскольку yield приостанавливает выполнение функции, поэтому можно использовать полученные lambdas до достижения конца функции, то есть до i равно 9. Чтобы увидеть, что это означает, рассмотрите следующие два примера использования test_with_yield:

[f(0) for f in test_with_yield()]
# Result: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

[f(0) for f in list(test_with_yield())]
# Result: [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]

Что здесь происходит, так это то, что первый пример дает lambda (while я равно 0), вызывает его (i все равно 0), затем продвигает функцию до тех пор, пока не будет получена другая lambda (i теперь 1), вызывает лямбда, и так далее. Важно то, что каждая лямбда вызывается до того, как поток управления вернется к test_with_yield (т.е. До изменения значения i).

Во втором примере мы сначала создаем список. Таким образом, первая лямбда дается (i равна 0) и помещается в список, вторая лямбда создается (теперь 1) и помещается в список... до тех пор, пока не будет получена последняя лямбда (теперь 9) и положите в список. И тогда мы начинаем называть лямбды. Так как i теперь 9, все lambdas возвращают 9.


¹ Важным здесь является то, что замыкания содержат ссылки на переменные, а не копии значения, которое они удерживали при создании замыкания. Таким образом, если вы назначаете переменную внутри лямбда (или внутренней функции, которая создает замыкания так же, как и lambdas), это также изменяет переменную за пределами лямбда, и если вы измените значение вне, это изменение будет видимый внутри лямбда.

Ответ 2

Нет, уступка не имеет ничего общего с закрытием.

Вот как распознать замыкания в Python: закрытие

  • функция

  • в котором выполняется поиск неквалифицированного имени.

  • не существует привязки имени в самой функции

  • но привязка имени существует в локальной области функции, определение которой окружает определение функции, в которой имя просматривается.

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

def lazy():
    return ( lambda x: x+i for i in range(10) )

def immediate():
    return [ lambda x: x+i for i in range(10) ]

def also_lazy():
    for i in range(10):
        yield lambda x:x+i

not_lazy_any_more = list(also_lazy())

print( [ f(10) for f in lazy()             ] ) # 10 -> 19
print( [ f(10) for f in immediate()        ] ) # all 19
print( [ f(10) for f in also_lazy()        ] ) # 10 -> 19
print( [ f(10) for f in not_lazy_any_more  ] ) # all 19 

Обратите внимание, что первый и третий примеры дают одинаковые результаты, как и второй и четвертый. Первый и третий ленивы, второй и четвертый - нет.

Обратите внимание, что все четыре примера предоставляют кучу замыканий по самой последней привязке i, это просто, что в первом третьем случае вы оцениваете закрытие перед повторным связыванием i (даже до того, как вы создали следующий закрытие в последовательности), а во втором и четвертом случае сначала вы ждете, пока i не отскочит до 9 (после того, как вы создали и собрали все блокировки, которые вы собираетесь делать), и только затем оцените закрытие.

Ответ 3

Добавляя к ответу @sepp2k, вы видите эти два разных поведения, потому что создаваемые функции lambda не знают, откуда они должны получить значение i. В то время, когда эта функция создается, все, что она знает, это то, что она должна либо получить значение i из локальной области, закрытой области видимости, глобальной области или встроенных функций.

В этом конкретном случае это переменная закрытия (закрытая область). И его значение меняется с каждой итерацией.


Проверьте LEGB на Python.


Теперь, почему второй работает как ожидалось, но не первый?

Это потому, что каждый раз, когда вы получаете функцию lambda, выполнение функции генератора прекращается в этот момент и когда вы вызываете его, и в этот момент оно будет использовать значение i. Но в первом случае мы уже продвинули значение i до 9, прежде чем вызов какой-либо из функций.

Чтобы доказать это, вы можете получить текущее значение i из содержимого ячейки __closure__:

>>> for func in test_with_yield():
        print "Current value of i is {}".format(func.__closure__[0].cell_contents)
        print func(9)
...
Current value of i is 0
Current value of i is 1
Current value of i is 2
Current value of i is 3
Current value of i is 4
Current value of i is 5
Current value of i is 6
...

Но вместо этого, если вы где-то храните функции и назовете их позже, вы увидите то же поведение, что и в первый раз:

from itertools import islice

funcs = []
for func in islice(test_with_yield(), 4):
    print "Current value of i is {}".format(func.__closure__[0].cell_contents)
    funcs.append(func)

print '-' * 20

for func in funcs:
    print "Now value of i is {}".format(func.__closure__[0].cell_contents)

Вывод:

Current value of i is 0
Current value of i is 1
Current value of i is 2
Current value of i is 3
--------------------
Now value of i is 3
Now value of i is 3
Now value of i is 3
Now value of i is 3

Пример, использованный Патриком Хоу в комментариях, также показывает то же самое: sum(t(1) for t in list(test_with_yield()))


Правильный способ:

Назначьте i в качестве значения по умолчанию для lambda, значения по умолчанию рассчитываются при создании функции, и они не будут меняться (если это не измененный объект). i теперь является локальной переменной для функций lambda.

>>> def test_without_closure():
        return [lambda x, i=i: x+i for i in range(10)]
...
>>> sum(t(1) for t in test_without_closure())
55