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

我预计4 。 我的推理是:在Python中,一切都是一个对象,因此每个variables都是必不可less的指针。 当为i创buildlambdaclosures时,我期望它存储一个指向i当前指向的整数对象。 这意味着当i分配一个新的整数对象时,它不应该影响以前创build的闭包。 可悲的是,在debugging器中检查adders数组显示它。 所有的lambda函数都指向i的最后一个值3 ,这导致adders[1](3)返回6

这使我想知道以下几点:

  • 什么closures准确捕获?
  • 说服lambda函数捕捉i的当前值的最优雅的方式是什么,当i改变它的值时不会受到影响?

你的第二个问题已经被回答了,但是至于你的第一个问题:

什么封闭准确捕获?

Python中的范围是dynamic的和词法的。 闭包将始终记住variables的名称和范围,而不是指向的对象。 由于您的示例中的所有函数都是在相同的作用域中创build的,并使用相同的variables名,所以它们始终引用相同的variables。

编辑:关于如何克服这个问题,有两种方法,想到:

  1. Adrien Plisson推荐的方法最简洁但并不严格等同。 创build一个具有额外参数的lambda,并将额外参数的默认值设置为要保留的对象。

  2. 稍微冗长一些,但不那么黑客就是每次创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