最近我开始摆弄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如何实现闭包的技术细节。参见早期绑定和晚期绑定的区别是什么?有关术语讨论。


当前回答

为了完整起见,第二个问题的另一个答案是:你可以在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)

其他回答

考虑下面的代码:

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

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

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)

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

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

闭包究竟捕获了什么?

Python中的闭包使用词法作用域:它们记住创建闭包变量的名称和作用域。但是,它们仍然是后期绑定:在使用闭包中的代码时查找名称,而不是在创建闭包时查找。由于示例中的所有函数都是在相同的作用域中创建的,并使用相同的变量名,因此它们总是引用相同的变量。

至少有两种方法可以获得早期绑定:

The most concise, but not strictly equivalent way is the one recommended by Adrien Plisson. Create a lambda with an extra argument, and set the extra argument's default value to the object you want preserved. More verbosely but also more robustly, we can create a new scope for each created 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 The scope here is created using a new function (another lambda, for brevity), which binds its argument, and passing the value you want to bind as the argument. In real code, though, you most likely will have an ordinary function instead of the lambda to create the new scope: 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的值)