(lambda)函数closures捕获什么?
最近我开始玩Python,并且在closures工作的时候遇到了一些奇怪的事情。 考虑下面的代码:
adders=[0,1,2,3] for i in [0,1,2,3]: adders[i]=lambda a: i+a print adders[1](3)
它构build了一个简单的函数数组,它接受一个单一的input并返回一个数字所添加的input。 这个函数是在for
循环里构造的,迭代器从0
到3
运行。 对于这些数字中的每一个,都会创build一个lambda
函数来捕获i
并将其添加到函数的input中。 最后一行以3
作为参数调用第二个lambda
函数。 令我惊讶的是,输出是6
。
我预计4
。 我的推理是:在Python中,一切都是一个对象,因此每个variables都是必不可less的指针。 当为i
创buildlambda
closures时,我期望它存储一个指向i
当前指向的整数对象。 这意味着当i
分配一个新的整数对象时,它不应该影响以前创build的闭包。 可悲的是,在debugging器中检查adders
数组显示它。 所有的lambda
函数都指向i
的最后一个值3
,这导致adders[1](3)
返回6
。
这使我想知道以下几点:
- 什么closures准确捕获?
- 说服
lambda
函数捕捉i
的当前值的最优雅的方式是什么,当i
改变它的值时不会受到影响?
你的第二个问题已经被回答了,但是至于你的第一个问题:
什么封闭准确捕获?
Python中的范围是dynamic的和词法的。 闭包将始终记住variables的名称和范围,而不是指向的对象。 由于您的示例中的所有函数都是在相同的作用域中创build的,并使用相同的variables名,所以它们始终引用相同的variables。
编辑:关于如何克服这个问题,有两种方法,想到:
-
Adrien Plisson推荐的方法最简洁但并不严格等同。 创build一个具有额外参数的lambda,并将额外参数的默认值设置为要保留的对象。
-
稍微冗长一些,但不那么黑客就是每次创buildlambda时创build一个新的作用域:
>>> 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,为了简洁)创build的,它绑定了它的参数,并将你想绑定的值作为parameter passing。 但是,在真实代码中,您很可能会使用普通函数而不是lambda来创build新范围:
def createAdder(x): return lambda y: y + x adders = [createAdder(i) for i in range(4)]
你可以用一个具有默认值的参数强制捕获一个variables:
>>> 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
),并给它一个你想捕获的variables的默认值( i
的值)
为了完整性,你的第二个问题的另一个答案:你可以在functools模块中使用partial 。
随着Chris Lutz提出的从运营商导入添加例子变成:
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为我们提供了operator
模块,它为基本运算符提供了一个function接口。 上面的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()
什么是closures?
>>> print f_1.func_closure, f_1.func_closure[0].cell_contents (<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43
值得注意的是,my_str不在f1中。
什么在f2的closures?
>>> 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的闭包中,而我不在f_3的闭包中(未显示),这表明闭包对象本身是不同的对象。
闭合对象本身是同一个对象吗?
>>> print f_1.func_closure is f_2.func_closure False