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循环中构造,其中迭代器i0运行到3。对于这些数字中的每一个都创建一个lambda函数,该函数捕获i并将其添加到函数的输入中。最后一行以3作为参数调用第二个lambda函数。令我惊讶的是,输出是6

我期望4。我的推理是:在Python中,所有东西都是对象,因此每个变量本质上都是指向对象的指针。在为i创建lambda闭包时,我期望它存储一个指向当前由i指向的整数对象的指针。这意味着当i分配一个新的整数对象时,它不应该影响之前创建的闭包。遗憾的是,在调试器中检查adders数组表明它是这样的。所有lambda函数都指向i的最后一个值,3,其结果是adders[1](3)返回lambda0。

这让我想知道以下几点:

  • 闭包究竟捕获了什么?
  • 说服lambda函数捕获i的当前值而不受i改变其值的影响的最优雅的方法是什么?

对于这个问题更容易理解、更实用的版本,具体到使用循环(或列表推导式、生成器表达式等)的情况,请参见在循环(或推导式)中创建函数(或lambdas) 。这个问题的重点是理解Python中代码的底层行为。

如果您在这里试图修复在Tkinter中制作按钮的问题,请尝试 Tkinter在for循环中创建按钮,传递命令参数以获得更具体的建议。

obj.__closure__中到底包含什么?如何 Python实现闭包的技术细节。见早期绑定和后期绑定的区别是什么?用于相关术语讨论。

71907 次浏览

在回答你的第二个问题时,最优雅的方法是使用一个接受两个形参的函数而不是数组:

add = lambda a, b: a + b
add(1, 3)

但是,在这里使用lambda有点愚蠢。Python为我们提供了operator模块,它为基本操作符提供了一个功能接口。上面的lambda只调用加法运算符就有不必要的开销:

from operator import add
add(1, 3)

我理解您在玩游戏,试图探索这门语言,但我无法想象在使用一组函数时,Python的奇怪作用域会成为阻碍。

如果你想,你可以写一个小的类,使用你的数组索引语法:

class Adders(object):
def __getitem__(self, item):
return lambda a: a + item


adders = Adders()
adders[1](3)

闭包究竟捕获了什么?

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

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

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

考虑下面的代码:

x = "foo"


def print_x():
print x


x = "bar"


print_x() # Outputs "bar"

我想大多数人都不会感到困惑。这是预期的行为。

那么,为什么人们认为在循环中完成会有所不同呢?我知道我自己犯了那个错误,但我不知道为什么。是循环吗?或者是?

毕竟,循环只是一个更短的版本:

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

为了完整起见,第二个问题的另一个答案是:你可以在functools模块中使用部分

使用Chris Lutz提出的import add from操作符,示例变成:

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)

下面是一个新示例,它突出显示了闭包的数据结构和内容,以帮助阐明何时“保存”封闭上下文。

def make_funcs():
i = 42
my_str = "hi"


f_one = lambda: i


i += 1
f_two = lambda: i+1


f_three = lambda: my_str
return f_one, f_two, f_three


f_1, f_2, f_3 = make_funcs()

闭包中包含什么?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

值得注意的是,my_str不在f1的闭包中。

f2的闭包里有什么?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

注意(从内存地址)两个闭包包含相同的对象。因此,你可以开始认为lambda函数具有对作用域的引用。然而,my_str不在f_1或f_2的闭包中,i也不在f_3的闭包中(未显示),这表明闭包对象本身是不同的对象。

闭包对象本身是相同的对象吗?

>>> print f_1.func_closure is f_2.func_closure
False

i的作用域进行排序的一种方法是在另一个作用域(闭包函数)中生成lambda,并将必要的参数交给它来生成lambda:

def get_funky(i):
return lambda a: i+a


adders=[None, None, None, None]


for i in [0,1,2,3]:
adders[i]=get_funky(i)


print(*(ar(5) for ar in adders))

当然给出5 6 7 8