【问题标题】:Why do Python yield statements form a closure?为什么 Python 的 yield 语句会形成一个闭包?
【发布时间】:2017-04-02 18:32:18
【问题描述】:

我有两个返回函数列表的函数。这些函数接受一个数字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 中 yielding 会形成闭包吗?

【问题讨论】:

  • 试试sum(t(1) for t in list(test_with_yield()))。你会得到100。当您在第二次求和中评估 t(1) 时,生成器尚未将 i 推进到下一个值。 test_with_yield 的执行被暂停并存储,直到请求下一个值。
  • 把 python 的闭包想象成总是在做 reference 复制,而不是 value 复制,你就会明白它的行为...

标签: python functional-programming closures yield


【解决方案1】:

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

这种情况在test_with_yield 中并没有太大的不同。那么,为什么会得到不同的结果呢?因为yield 暂停了函数的运行,所以可以在到达函数末尾之前使用yield lambdas,即在i 为9 之前。要了解这意味着什么,请考虑以下两个使用@ 的示例987654326@:

[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(当 i 为 0 时),调用它(i 仍然是 0),然后推进函数直到产生另一个 lambda(i 现在是 1),调用 lambda,等等。重要的是每个 lambda 在控制流返回到test_with_yield 之前被调用(即在 i 的值改变之前)。

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


¹ 这里的重要一点是闭包持有对变量的引用,而不是创建闭包时它们持有的值的副本。这样,如果您分配给 lambda 内部的变量(或内部函数,它创建闭包的方式与 lambda 相同),这也会更改 lambda 外部的变量,如果您在外部更改值,则该更改将是在 lambda 中可见。

【讨论】:

  • 见这里:docs.python-guide.org/en/latest/writing/gotchas section "Late Binding Closures"
  • @PatrickHaugh [lambda x, i=i: x+i for i in range(10)] 将返回预期的 lambdas。
  • @MoinuddinQuadri lambda 不包含 i 的任何值,它们包含对 i 的引用。因此,如果 i 发生更改,则该更改通过该引用可见。
  • “在“test_without_closure”中得到所有 9 的原因并不是没有闭包,这是不正确的。 lambda both 函数中对 i 的闭包。
  • @jacg,我认为你误读了双重否定。发问者显然认为没有关闭,但这是错误的,所以这不是全 9 结果的原因;原因是所有的 lambdas 都是闭包,正如你们所说的那样。
【解决方案2】:

不,屈服与闭包无关。

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

  1. 一个函数

  2. 执行非限定名称查找

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

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

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

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 in Python


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

    这是因为每次生成 lambda 函数时,生成器函数的执行都会在那个时刻停止,当你调用它时,它会在那个时刻使用 i 的值。但在第一种情况下,在调用任何函数之前,我们已经将 i 的值提高到 9。

    为了证明这一点,您可以从__closure__ 的单元格内容中获取i 的当前值:

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

    【讨论】:

      猜你喜欢
      • 2020-04-07
      • 1970-01-01
      • 2013-02-09
      • 2014-05-22
      • 2021-01-01
      • 2011-02-22
      • 2014-06-04
      • 2017-09-09
      • 2012-09-14
      相关资源
      最近更新 更多