为什么 Python yield 语句形成闭包?
Why do Python yield statements form a closure?
我有两个函数,return一个函数列表。这些函数接受一个数字 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
到 return 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 中 yield 是否形成闭包?
Yielding 在 Python 中不创建闭包,lambdas 创建闭包。在 "test_without_closure" 中得到全 9 的原因并不是没有闭包。如果没有,您根本无法访问 i
。问题是所有闭包都包含对同一个 i 变量的引用¹,该变量在函数末尾为 9。
这种情况在 test_with_yield
中并没有太大的不同。那么,为什么会得到不同的结果呢?因为 yield
暂停了函数的 运行,所以可以在函数结束之前使用生成的 lambda,即在 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(当 i 为 0),调用它(i 仍然是 0),然后推进函数直到产生另一个 lambda(我现在为 1),调用拉姆达,等等。重要的是每个 lambda 在控制流 returns 到 test_with_yield
之前被调用(即在 i 的值改变之前)。
在第二个例子中,我们首先创建一个列表。所以第一个 lambda 被产生(i 是 0)并放入列表,第二个 lambda 被创建(我现在是 1)并被放入列表......直到最后一个 lambda 被产生(我现在是 9)并放入进入列表。 然后 我们开始调用 lambda。所以由于 i
现在是 9,所以所有的 lambdas return 9.
¹ 这里重要的一点是闭包保存对变量的引用,而不是它们在创建闭包时保存的值的副本。这样,如果您在 lambda 内部(或内部函数,它以与 lambda 相同的方式创建闭包)赋值给变量,这也会更改 lambda 外部的变量,如果您更改外部的值,则该更改将是在 lambda 内部可见。
添加到@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
使用的示例也显示了相同的内容: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
不,屈服与闭包无关。
以下是如何识别 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
的 最近 绑定上提供了一堆闭包,只是在第一个和第三个案例中你评估了闭包before rebinding i
(甚至在你创建序列中的下一个闭包之前),而在第二种和第四种情况下,你首先等到 i
有已经反弹到 9(在你创建并收集了你要创建的所有闭包之后),然后才评估闭包。
我有两个函数,return一个函数列表。这些函数接受一个数字 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
到 return 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 中 yield 是否形成闭包?
Yielding 在 Python 中不创建闭包,lambdas 创建闭包。在 "test_without_closure" 中得到全 9 的原因并不是没有闭包。如果没有,您根本无法访问 i
。问题是所有闭包都包含对同一个 i 变量的引用¹,该变量在函数末尾为 9。
这种情况在 test_with_yield
中并没有太大的不同。那么,为什么会得到不同的结果呢?因为 yield
暂停了函数的 运行,所以可以在函数结束之前使用生成的 lambda,即在 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(当 i 为 0),调用它(i 仍然是 0),然后推进函数直到产生另一个 lambda(我现在为 1),调用拉姆达,等等。重要的是每个 lambda 在控制流 returns 到 test_with_yield
之前被调用(即在 i 的值改变之前)。
在第二个例子中,我们首先创建一个列表。所以第一个 lambda 被产生(i 是 0)并放入列表,第二个 lambda 被创建(我现在是 1)并被放入列表......直到最后一个 lambda 被产生(我现在是 9)并放入进入列表。 然后 我们开始调用 lambda。所以由于 i
现在是 9,所以所有的 lambdas return 9.
¹ 这里重要的一点是闭包保存对变量的引用,而不是它们在创建闭包时保存的值的副本。这样,如果您在 lambda 内部(或内部函数,它以与 lambda 相同的方式创建闭包)赋值给变量,这也会更改 lambda 外部的变量,如果您更改外部的值,则该更改将是在 lambda 内部可见。
添加到@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
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
不,屈服与闭包无关。
以下是如何识别 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
的 最近 绑定上提供了一堆闭包,只是在第一个和第三个案例中你评估了闭包before rebinding i
(甚至在你创建序列中的下一个闭包之前),而在第二种和第四种情况下,你首先等到 i
有已经反弹到 9(在你创建并收集了你要创建的所有闭包之后),然后才评估闭包。