为什么Python yield语句形成一个闭包?

时间:2016-11-18 19:53:29

标签: python functional-programming closures yield

我有两个函数返回一个函数列表。这些函数使用数字x并向其添加ii是一个从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中形成一个闭包?

3 个答案:

答案 0 :(得分:29)

Yielding不会在Python中创建闭包,lambdas会创建一个闭包。你在“test_without_closure”中得到所有9个的原因并不是没有关闭。如果没有,您根本无法访问i。问题是所有闭包都包含对同一个i变量的引用¹,该函数在函数末尾为9。

这种情况在test_with_yield中差别不大。那么,为什么你会得到不同的结果?因为yield暂停函数的运行,所以在函数结束之前,即在i为9之前,可以使用生成的lambdas。要查看这意味着什么,请考虑以下两个使用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(而我是0),调用它(我仍然是0),然后推进该函数直到另一个lambda被产生(我现在是1),调用lambda,等等。重要的是在控制流返回test_with_yield之前(即在i的值改变之前)调用每个lambda。

在第二个例子中,我们首先创建一个列表。所以第一个lambda被生成(i为0)并放入列表中,第二个lambda被创建(我现在是1)并被放入列表中......直到最后一个lambda被生成(我现在是9)并且放入进入清单。然后然后我们开始调用lambdas。所以,由于i现在为9,所有lambda都返回9。

¹这里重要的一点是闭包持有对变量的引用,而不是创建闭包时它们所持有的值的副本。这样,如果你在lambda(或者内部函数中)中分配变量,它就像lambdas那样创建闭包,这也会改变lambda之外的变量,如果你改变外面的值,那么这个变化将是在lambda内可见。

答案 1 :(得分:7)

不,屈服与闭包无关。

以下是如何识别Python中的闭包:闭包是

  1. 一个功能

  2. 执行非限定名称查找

  3. 函数本身不存在名称绑定

  4. 但名称的绑定存在于函数的本地范围内,该函数的定义包含查找名称的函数的定义。

  5. 您观察到的行为差异的原因是懒惰,而不是与闭包有关。比较和对比以下

    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最新绑定的闭包,只是在第一个第三个案例中,您在之前评估闭包 / em>重新绑定i(甚至在您创建序列中的下一个闭包之前),而在第二个和第四个案例中,您首先等待i反弹到9(在您之后)我创建并收集了你要制作的所有闭包,然后只评估闭包。

答案 2 :(得分:3)

添加到@ sepp2k的答案你会看到这两种不同的行为,因为正在创建的lambda函数不知道它们必须从哪里得到i的值。在创建此函数时,它只知道它必须从本地范围,封闭范围,全局范围或内置函数中获取i的值。

在这种特殊情况下,它是一个闭包变量(封闭范围)。它的价值随着每次迭代而变化。

查看LEGB in 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

Patrick Haugh in comments使用的示例也显示了相同的内容:sum(t(1) for t in list(test_with_yield()))

正确的方式:

i指定为lambda的默认值,默认值是在创建函数时计算的,并且不会更改(unless it's a mutable object)。 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