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

我有两个函数返回一个函数列表。 该功能采取了一些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中产生闭包?


Yielding不会在Python中创建闭包,lambda会创建闭包。 你在“test_without_closure”中获得所有9的原因并不是没有关闭。 如果没有,你根本无法访问i 。 问题是所有的闭包都包含一个相同的i变量的参考,这个变量在函数的结尾是9。

这种情况在test_with_yield没有太大的区别。 那么,为什么你会得到不同的结果? 由于yield暂停了函数的运行,因此可以在函数结束之前(即i之前)使用已生成的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,等等。 重要的是每个lambda在控制流返回到test_with_yield之前被调用(即在i的值改变之前)。

在第二个例子中,我们首先创建一个列表。 因此,第一个lambda被放弃(我是0)并放入列表中,第二个lambda被创建(我现在是1)并放入列表中...直到最后一个lambda被放弃(我现在是9)并放入列入清单。 然后我们开始调用lambda表达式。 所以,因为i现在是9,所有的lambda都返回9。


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


不,屈服与封闭无关。

以下是如何识别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(在你创建并收集了你将要完成的所有闭包之后),然后才评估闭包。


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

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


    在Python中查看LEGB。


    现在为什么第二个按预期工作,但不是第一个?

    这是因为每次你产生一个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在注释中使用的示例还显示了同样的事情: 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
    
    链接地址: http://www.djcxy.com/p/53149.html

    上一篇: Why do Python yield statements form a closure?

    下一篇: Is there a need to close files that have no reference to them?