最近我开始摆弄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并将其添加到函数的输入中。最后一行以3作为参数调用第二个lambda函数。令我惊讶的是输出是6。

I expected a 4. My reasoning was: in Python everything is an object and thus every variable is essential a pointer to it. When creating the lambda closures for i, I expected it to store a pointer to the integer object currently pointed to by i. That means that when i assigned a new integer object it shouldn't effect the previously created closures. Sadly, inspecting the adders array within a debugger shows that it does. All lambda functions refer to the last value of i, 3, which results in adders[1](3) returning 6.

这让我想知道以下几点:

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


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

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

查看到底是什么包含在obj.__closure__?以了解Python如何实现闭包的技术细节。参见早期绑定和晚期绑定的区别是什么?有关术语讨论。


当前回答

考虑下面的代码:

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

其他回答

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

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

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

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

但是,在这里使用lambda有点愚蠢。Python为我们提供了操作符模块,该模块为基本操作符提供了功能接口。上面的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)

一种整理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。

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

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

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

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

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

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

使用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)