lambda函数闭包捕获了什么?
最近我开始玩Python,发现了闭包工作的一些奇怪之处。考虑以下代码:
adders=[None, None, None, None]
for i in [0,1,2,3]:
adders[i]=lambda a: i+a
print adders[1](3)
这段代码创建了一个简单的函数数组,每个函数接受一个输入,并返回这个输入加上一个数字。这些函数是在一个for
循环中构建的,循环变量i
从0
到3
。对于这些数字,每次都会创建一个lambda
函数,它会捕获i
的值,并将其加到函数的输入上。最后一行调用了第二个lambda
函数,并传入3
作为参数。令我惊讶的是,输出结果是6
。
我原本以为结果是4
。我的想法是:在Python中,一切都是对象,因此每个变量本质上都是指向对象的指针。当为i
创建lambda
闭包时,我以为它会存储一个指向当前i
所指向的整数对象的指针。这意味着,当i
被赋值为一个新的整数对象时,不应该影响之前创建的闭包。可惜的是,在调试器中检查adders
数组时发现并不是这样。所有的lambda
函数都引用了i
的最后一个值3
,这导致adders[1](3)
返回6
。
这让我想到了以下问题:
- 闭包到底捕获了什么?
- 有没有优雅的方法让
lambda
函数捕获i
的当前值,以便在i
改变时不受影响?
如果你想要一个更易懂、实用的版本,特别是关于循环(或列表推导、生成器表达式等)的情况,可以查看 在循环中创建函数(或lambda)。这个问题主要关注理解Python代码的底层行为。
如果你是为了修复Tkinter中按钮的问题而来到这里,可以查看 tkinter在for循环中创建按钮并传递命令参数,获取更具体的建议。
有关Python如何实现闭包的技术细节,请查看 obj.__closure__中到底包含了什么?。有关相关术语的讨论,请查看 早绑定和晚绑定有什么区别?
8 个回答
为了让你的第二个问题更完整,这里还有一个答案:你可以在 functools 模块中使用 partial。
按照 Chris Lutz 的建议,导入 operator 模块中的 add 后,示例代码变成了:
from functools import partial
from operator import add # add(a, b) -- Same as a + b.
adders = [0,1,2,3]
for i in [0,1,2,3]:
# store callable object with first argument given as (current) i
adders[i] = partial(add, i)
print adders[1](3)
你可以通过使用一个带默认值的参数来强制捕获一个变量:
>>> for i in [0,1,2,3]:
... adders[i]=lambda a,i=i: i+a # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4
这个想法是声明一个参数(聪明地命名为 i
),并给它一个你想要捕获的变量的默认值(也就是 i
的值)
闭包到底捕获了什么呢?
在Python中,闭包使用的是词法作用域:它们记住了被捕获变量的名字和创建时的作用域。不过,它们仍然是晚绑定的:变量的名字是在闭包中的代码被使用时查找的,而不是在闭包创建时查找的。因为你例子中的所有函数都是在同一个作用域内创建的,并且使用了相同的变量名,所以它们总是指向同一个变量。
如果想要实现早绑定,至少有两种方法:
最简洁,但不完全等价的方法是Adrien Plisson推荐的方式。创建一个带有额外参数的lambda,并将这个额外参数的默认值设置为你想要保留的对象。
虽然更啰嗦,但也更稳妥,我们可以为每个创建的lambda创建一个新的作用域:
>>> adders = [0,1,2,3] >>> for i in [0,1,2,3]: ... adders[i] = (lambda b: lambda a: b + a)(i) ... >>> adders[1](3) 4 >>> adders[2](3) 5
这里的作用域是通过一个新函数(为了简洁用另一个lambda)创建的,它绑定了它的参数,并将你想要绑定的值作为参数传入。不过在实际代码中,你很可能会用一个普通函数来创建新的作用域,而不是lambda:
def createAdder(x): return lambda y: y + x adders = [createAdder(i) for i in range(4)]