最近我开始摆弄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如何实现闭包的技术细节。参见早期绑定和晚期绑定的区别是什么?有关术语讨论。
闭包究竟捕获了什么?
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)]
下面是一个新示例,它突出显示了闭包的数据结构和内容,以帮助阐明何时“保存”封闭上下文。
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