lambda函数闭包捕获了什么?

374 投票
8 回答
85897 浏览
提问于 2025-04-15 19:28

最近我开始玩Python,发现了闭包工作的一些奇怪之处。考虑以下代码:

adders=[None, None, None, None]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

这段代码创建了一个简单的函数数组,每个函数接受一个输入,并返回这个输入加上一个数字。这些函数是在一个for循环中构建的,循环变量i03。对于这些数字,每次都会创建一个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 个回答

53

为了让你的第二个问题更完整,这里还有一个答案:你可以在 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)
326

你可以通过使用一个带默认值的参数来强制捕获一个变量:

>>> 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 的值)

229

闭包到底捕获了什么呢?

在Python中,闭包使用的是词法作用域:它们记住了被捕获变量的名字和创建时的作用域。不过,它们仍然是晚绑定的:变量的名字是在闭包中的代码被使用时查找的,而不是在闭包创建时查找的。因为你例子中的所有函数都是在同一个作用域内创建的,并且使用了相同的变量名,所以它们总是指向同一个变量。

如果想要实现早绑定,至少有两种方法:

  1. 最简洁,但不完全等价的方法是Adrien Plisson推荐的方式。创建一个带有额外参数的lambda,并将这个额外参数的默认值设置为你想要保留的对象。

  2. 虽然更啰嗦,但也更稳妥,我们可以为每个创建的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)]
    

撰写回答