提问人:Boaz 提问时间:2/19/2010 最后编辑:Karl KnechtelBoaz 更新时间:6/24/2023 访问量:82126
lambda 函数闭包捕获什么?
What do lambda function closures capture?
问:
最近我开始玩 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
lambda
3
6
我期待一个.我的理由是:在 Python 中,一切都是一个对象,因此每个变量都是必不可少的指针。在为 创建闭包时,我希望它存储一个指向当前指向的整数对象的指针。这意味着,当分配一个新的整数对象时,它不应该影响以前创建的闭包。可悲的是,在调试器中检查数组表明它确实如此。所有函数都引用 的最后一个值 ,这会导致返回 。4
lambda
i
i
i
adders
lambda
i
3
adders[1](3)
6
这让我想知道以下几点:
- 闭合究竟捕获了什么?
- 说服函数以一种在更改其值时不会受到影响的方式捕获当前值的最优雅的方法是什么?
lambda
i
i
有关该问题的更易于理解的实用版本,特定于使用循环(或列表推导式、生成器表达式等)的情况,请参阅在循环(或推导式)中创建函数(或 lambda)。这个问题的重点是理解 Python 中代码的底层行为。
如果你来到这里试图解决在 Tkinter 中制作按钮的问题,请尝试在 for 循环中创建按钮,传递命令参数以获得更具体的建议。
有关 Python 如何实现闭包的技术细节,请参阅obj.__closure__中究竟包含哪些内容?有关相关术语的讨论,请参阅早期绑定和晚期绑定之间有什么区别?
答:
在回答第二个问题时,最优雅的方法是使用一个接受两个参数而不是数组的函数:
add = lambda a, b: a + b
add(1, 3)
但是,在这里使用 lambda 有点傻。Python 为我们提供了模块,它为基本运算符提供了一个功能接口。上面的 lambda 有不必要的开销,只是为了调用加法运算符:operator
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)
评论
闭合究竟捕获了什么?
Python 中的闭包使用词法范围:它们记住创建它的闭包变量的名称和范围。但是,它们仍然是后期绑定:在使用闭包中的代码时查找名称,而不是在创建闭包时查找名称。由于示例中的所有函数都是在相同的范围内创建的,并且使用相同的变量名称,因此它们始终引用相同的变量。
至少有两种方法可以改为获得早期绑定:
最简洁但并非严格等效的方式是 Adrien Plisson 推荐的方法。创建一个带有额外参数的 lambda,并将额外参数的默认值设置为要保留的对象。
我们可以更详细但更健壮地为每个创建的 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
此处的范围是使用一个新函数(为简洁起见,另一个 lambda)创建的,该函数绑定其参数,并将要绑定的值作为参数传递。但是,在实际代码中,您很可能会使用一个普通函数而不是 lambda 来创建新范围:
def createAdder(x): return lambda y: y + x adders = [createAdder(i) for i in range(4)]
评论
set!
您可以使用具有默认值的参数强制捕获变量:
>>> 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
)
评论
请考虑以下代码:
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
评论
i
lambda
lambda
lambda
__init__
self
self
NameError 引发 NameError
?答案当然是关闭。adders[1](3)
4
i
return
adders
为了完整起见,第二个问题的另一个答案是:可以在 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)
评论
下面是一个新示例,它突出显示了闭包的数据结构和内容,以帮助阐明闭包上下文何时“保存”。
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
评论
int object at [address X]>
整理作用域的一种方法是在另一个作用域(闭包函数)中生成 lambda,并交出必要的参数来生成 lambda:i
def get_funky(i):
return lambda a: i+a
adders=[None, None, None, None]
for i in [0,1,2,3]:
adders[i]=get_funky(i)
print(*(ar(5) for ar in adders))
当然是给予。5 6 7 8
评论
在函数中创建加法器以捕获值:
def create_adder(i):
return lambda a: i + a
if __name__ == '__main__':
adders = [None, None, None, None]
for i in [0, 1, 2, 3]:
adders[i] = create_adder(i)
print(adders[1](3))
评论
i
print i