问题:(lambda)函数闭包捕获了什么?
最近,我开始玩弄Python,并且在闭包的工作方式中遇到了一些奇怪的事情。考虑以下代码:
adders=[0,1,2,3]
for i in [0,1,2,3]:
adders[i]=lambda a: i+a
print adders[1](3)
它构建了一个简单的函数数组,这些函数接受单个输入并返回该输入加数字后的结果。这些函数在for
循环中构造,其中迭代器i
从0
到运行3
。对于这些数字中的每一个,lambda
都会创建一个函数,将其捕获i
并将其添加到函数的输入中。最后一行使用参数作为参数调用第二个lambda
函数3
。令我惊讶的Yield6
。
我期望一个4
。我的推论是:在Python中,一切都是对象,因此每个变量都是指向它的指针。为创建lambda
闭包时i
,我希望它存储一个指向当前由指向的整数对象的指针i
。这意味着,当i
分配一个新的整数对象时,它不应影响先前创建的闭包。可悲的是,adders
在调试器中检查该阵列是否可以完成。所有的lambda
功能指的最后一个值i
,3
,其结果adders[1](3)
返回6
。
这让我想知道以下几点:
- 闭包到底捕获了什么?
- 用最优雅的方法说服
lambda
功能捕获当前值,i
而该方法在i
更改其值时不会受到影响?
回答 0
您的第二个问题已经回答,但第一个问题是:
闭包究竟捕获了什么?
Python的作用域是动态且词汇丰富的。闭包将始终记住变量的名称和范围,而不是其指向的对象。由于示例中的所有函数都是在同一作用域中创建的,并且使用相同的变量名,因此它们始终引用相同的变量。
编辑:关于您如何解决此问题的其他问题,有两种方法可以想到:
最简洁但并非严格等效的方法是Adrien Plisson推荐的方法。创建带有额外参数的lambda,并将额外参数的默认值设置为要保留的对象。
每次创建lambda时,创建一个新的作用域会更加冗长一些,但hacky会更少:
>>> 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来创建新范围:
def createAdder(x): return lambda y: y + x adders = [createAdder(i) for i in range(4)]
回答 1
您可以使用具有默认值的参数来强制捕获变量:
>>> 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
)
回答 2
为了完整起见,第二个问题的另一个答案是:您可以在functools模块中使用partial。
通过像Chris Lutz所建议的那样从运算符导入add,示例变为:
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)
回答 3
考虑以下代码:
x = "foo"
def print_x():
print x
x = "bar"
print_x() # Outputs "bar"
我认为大多数人都不会觉得这令人困惑。这是预期的行为。
那么,为什么人们认为循环完成会有所不同呢?我知道我自己犯了这个错误,但我不知道为什么。是循环吗?也许是lambda?
毕竟,循环只是以下内容的简化版本:
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
回答 4
为了回答第二个问题,最优雅的方法是使用一个接受两个参数而不是数组的函数:
add = lambda a, b: a + b
add(1, 3)
但是,在这里使用lambda有点愚蠢。Python为我们operator
提供了该模块,该模块为基本运算符提供了功能接口。上面的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)
回答 5
这是一个新的示例,突出显示了闭包的数据结构和内容,以帮助阐明何时“保存”了封闭的上下文。
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
从内存地址中注意到,两个闭包都包含相同的对象。所以,你可以开始将lambda函数视为对范围的引用。但是,my_str不在f_1或f_2的闭包中,i不在f_3的闭包中(未显示),这表明闭包对象本身是不同的对象。
闭包对象本身是否是同一对象?
>>> print f_1.func_closure is f_2.func_closure
False