Python中奇怪的闭包行为

13

我有以下简单的代码:

def get():
    return [lambda: i for i in [1, 2, 3]]

for f in get():
    print(f())

从我的python知识来看,输出结果是3 - 整个列表将包含i的最后一个值。但这是如何在内部工作的呢?

据我所知,Python变量只是对象的引用,因此第一个闭包必须首先包含对象i的引用 - 而这个对象明显是1而不是3 O_O。为什么Python闭包会封闭变量本身而不是这个变量引用的对象?它保存变量名作为纯文本、某种“对变量的引用”还是其他什么?

3个回答

13

正如@thg435所指出的那样,lambda函数不会封装那个时刻的值,而是作用域。有两种小的方式可以解决这个问题:

lambda默认参数“hack”

[ lambda v=i: v for i in [ 1, 2, 3 ] ]

或者使用 functools.partial

from functools import partial
[ partial(lambda v: v, i) for i in [ 1, 2, 3 ] ]

本质上,您需要将作用域移动到创建的函数的局部范围。通常我更喜欢经常使用 partial ,因为您可以将可调用对象以及任何参数和kargs传递给它,从而创建一个具有正确闭包的可调用对象。在内部,它会包装您原始的可调用对象,使得作用域对您进行了转换。


5
在这里使用partial,我认为这是一个更清晰的方法。不过你可能需要编辑你的代码,因为根据PEP-8,列表括号内的空格是不好的。(我知道你正在跟随提问者,我已经在那里进行了编辑)。 - Gareth Latty
@Lattyware:谢谢!lambdas很酷,但我总觉得它们阅读起来更凌乱。partial函数感觉更易读和可移植。 - jdi
我完全同意,我尽可能避免使用lambda表达式,在这种情况下,partial更清晰、更简单。 - Gareth Latty

10

闭包不是引用变量,而是引用作用域。由于在其作用域中最后一个 i 的值为“3”,因此所有三个闭包都返回相同的值。要“锁定”变量的当前值,请为其创建一个新的作用域:

def get() : return [ (lambda x: lambda: x)(i) for i in [ 1, 2, 3 ] ]
for f in get() : print( f() )

2
"(lambda x: lambda : x)(i)"是我在Python中看到的最丑陋的代码之一。呃。并不是说你的答案是错误或者不好,只是说——它很难读懂。 - Gareth Latty
3
@EyeofHell: 我认为pep-227是关于Python作用域规则的权威文档。此外,在Stack Overflow上也有一些很好的答案,例如这里 - georg
2
@GregE。我认为在Python中漂亮地完成这个任务并非不可能。我认为jdi的答案,将functools.partial()lambda混合使用是最好的解决方案,它以更加优美的方式描述了正在进行的操作 - 从我的角度来看。 - Gareth Latty
@Lattyware,确实更好,我并不是说以一个相当清晰的方式做这件事是不可能的。然而,它需要导入一个库,并且不能使用核心语言结构来实现,这表明它从未成为主要的设计重点。我非常喜欢Python,但它一直对函数式编程所必需的语言特性持有轻视甚至敌对的态度。 - Greg E.
@EyeofHell:顺便说一下,我对这个工作原理的理解可能不完全正确。我发布了一个后续问题,希望有人能提供更好的解释。 - georg
显示剩余4条评论

4
每个 lambda 实际上都是引用由列表推导式创建的变量 i。在列表推导式终止时,i 保留了它被分配到的最后一个元素的值,直到它超出作用域(这可以通过将其封装在函数内并返回它,即 lambda 来防止)。正如其他人指出的那样,闭包不会维护值的副本,而是维护对在其范围内定义的变量的引用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接