What do (lambda) function closures capture?
最近我开始使用python,在闭包的工作方式中我遇到了一些特殊的问题。请考虑以下代码:
1 2 3 4 5 6 | adders=[0,1,2,3] for i in [0,1,2,3]: adders[i]=lambda a: i+a print adders[1](3) |
它构建了一个简单的函数数组,这些函数接受一个输入并返回由一个数字添加的输入。函数在
我以为会有一辆
这让我想知道以下几点:
- 闭包到底捕获了什么?
- 说服
lambda 函数捕捉i 的当前值的最优雅的方法是什么?这种方法在i 更改值时不会受到影响?
可以使用具有默认值的参数强制捕获变量:
1 2 3 4 5 | >>> 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 |
其思想是声明一个参数(巧妙地命名为
你的第二个问题已经回答了,但第一个问题是:
what does the closure capture exactly?
python中的作用域是dynamic andlexical。闭包将始终记住变量的名称和范围,而不是它所指向的对象。由于示例中的所有函数都是在同一范围内创建的,并且使用相同的变量名,因此它们总是引用相同的变量。
编辑:关于如何克服这一问题的另一个问题,有两种方法:
最简洁但并非严格等效的方法是阿德里安·普里松推荐的方法。用一个额外参数创建一个lambda,并将这个额外参数的默认值设置为要保留的对象。
在每次创建lambda时,创建一个新的作用域会更加详细,但不那么容易被黑客攻击:
1 2 3 4 5 6 7 8 | >>> 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,为了简洁起见)创建的,它绑定了它的参数,并传递您想要绑定的值作为参数。不过,在真正的代码中,您很可能使用普通函数而不是lambda来创建新的作用域:
1 2 3 | def createAdder(x): return lambda y: y + x adders = [createAdder(i) for i in range(4)] |
为了完整性,第二个问题的另一个答案是:您可以在functools模块中使用分部。
根据Chris Lutz提出的从操作符导入add,示例如下:
1 2 3 4 5 6 7 8 9 | 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) |
请考虑以下代码:
1 2 3 4 5 6 7 8 | x ="foo" def print_x(): print x x ="bar" print_x() # Outputs"bar" |
我想大多数人根本不会觉得这令人困惑。这是预期的行为。
那么,为什么人们认为当它在一个循环中完成时会有所不同呢?我知道我自己犯了那个错误,但我不知道为什么。是环路吗?或者是lambda?
毕竟,循环只是一个较短的版本:
1 2 3 4 5 6 7 8 9 | 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 |
下面是一个新的示例,它突出显示了闭包的数据结构和内容,以帮助澄清何时"保存"了封闭上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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() |
什么是结束语?
1 2 | >>> print f_1.func_closure, f_1.func_closure[0].cell_contents (<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 |
值得注意的是,我的"str"并没有关闭F1。
二层楼的封闭部分是什么?
1 2 | >>> print f_2.func_closure, f_2.func_closure[0].cell_contents (<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 |
注意(从内存地址)两个闭包都包含相同的对象。因此,可以开始将lambda函数看作是对作用域的引用。但是,我的str不在f_1或f_2的闭包中,我也不在f_3的闭包中(未显示),这表明闭包对象本身是不同的对象。
闭包对象本身是同一对象吗?
1 2 | >>> print f_1.func_closure is f_2.func_closure False |
在回答第二个问题时,最优雅的方法是使用一个采用两个参数而不是数组的函数:
1 2 | add = lambda a, b: a + b add(1, 3) |
然而,在这里使用lambda有点傻。python为我们提供了
1 2 | from operator import add add(1, 3) |
我知道你在玩弄,试图探索语言,但我无法想象我会使用一系列函数,在这些函数中,python的作用域怪异会妨碍到你。
如果需要,可以编写一个使用数组索引语法的小类:
1 2 3 4 5 6 | class Adders(object): def __getitem__(self, item): return lambda a: a + item adders = Adders() adders[1](3) |