(lambda)函数闭包捕获了什么?


249

最近,我开始玩弄Python,并且在闭包的工作方式中遇到了一些奇怪的事情。考虑以下代码:

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

它构建了一个简单的函数数组,这些函数接受单个输入并返回该输入加数字后的结果。这些函数在for循环中构造,其中迭代器i0到运行3。对于这些数字中的每一个,lambda都会创建一个函数,将其捕获i并将其添加到函数的输入中。最后一行使用参数作为参数调用第二个lambda函数3。令我惊讶的产量6

我期望一个4。我的推论是:在Python中,一切都是对象,因此每个变量都是指向它的指针。为创建lambda闭包时i,我希望它存储一个指向当前由指向的整数对象的指针i。这意味着,当i分配一个新的整数对象时,它不应影响先前创建的闭包。可悲的是,adders在调试器中检查该阵列是否可以完成。所有的lambda功能指的最后一个值i3,其结果adders[1](3)返回6

这让我想知道以下几点:

  • 闭包到底捕获了什么?
  • 用最优雅的方法说服lambda功能捕获当前值,i而该方法在i更改其值时不会受到影响?

34
我在UI代码中遇到了这个问题。让我疯了。诀窍是要记住,循环不会创建新的作用域。
10

3
@TimMB如何i离开命名空间?
2013年

3
@detly好吧,我要说的是print i循环后将不起作用。但是我自己进行了测试,现在我明白了您的意思-它确实有效。我不知道循环变量在python的循环体之后仍然存在。
Tim MB

1
@TimMB-是的,这就是我的意思。同为ifwithtry等等
detly

13
这在官方Python FAQ中的“ 为什么为什么在循环中定义的具有不同值的lambda全部返回相同的结果?” ,并附有说明和通常的解决方法。
2014年

Answers:


161

您的第二个问题已经回答,但第一个问题是:

闭包究竟捕获了什么?

Python的作用域是动态且词汇丰富的。闭包将始终记住变量的名称和范围,而不是其指向的对象。由于示例中的所有函数都是在同一作用域中创建的,并且使用相同的变量名,因此它们始终引用相同的变量。

编辑:关于您如何解决此问题的其他问题,有两种方法可以想到:

  1. 最简洁但并非严格等效的方法是Adrien Plisson推荐的方法。创建带有额外参数的lambda,并将额外参数的默认值设置为要保留的对象。

  2. 每次创建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
最多,如果您为我的其他答案(简单问题)添加答案,我可以将其标记为可接受的答案。谢谢!
波阿斯

3
Python具有静态作用域,而不是动态作用域。它只是所有变量都是引用,因此,当您将变量设置为新对象时,变量本身(引用)具有相同的位置,但指向其他对象。如果您在Scheme中也会发生相同的情况set!。有关动态范围的真正含义,请参见此处:voidspace.org.uk/python/articles/code_blocks.shtml
Claudiu 2010年

6
选项2类似于功能语言称为“ Curried函数”。
Crashworks

205

您可以使用具有默认值的参数来强制捕获变量:

>>> 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


7
+1用于使用默认值。在定义lambda时对其进行评估,使它们非常适合此用途。
quornian

21
+1也是因为这是官方FAQ认可的解决方案。
2014年

23
这真太了不起了。但是,默认的Python行为不是。
塞西尔·库里

1
但是,这似乎并不是一个好的解决方案……您实际上是为了捕获变量的副本而更改函数签名。而且那些调用该函数的人也会搞乱i变量,对吗?
David Callanan

@DavidCallanan我们谈论的是lambda:通常在您自己的代码中定义的一种即席函数,用于插入漏洞,而不是您在整个sdk中共享的一种。如果需要更强的签名,则应使用实函数。
艾德里安·普里森

33

为了完整起见,第二个问题的另一个答案是:您可以在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)

23

考虑以下代码:

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

11
这是循环,因为在许多其他语言中,循环可以创建新的作用域。
2010年

1
这个答案很好,因为它解释了为什么i每个lambda函数都访问相同的变量。
David Callanan

3

为了回答第二个问题,最优雅的方法是使用一个接受两个参数而不是数组的函数:

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)

2
克里斯,当然上面的代码与我最初的问题无关。它旨在以一种简单的方式说明我的观点。这当然是毫无意义和愚蠢的。
波阿斯

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]>使我认为闭包正在存储[地址X] AKA引用。但是,如果在lambda语句之后重新分配了变量,则[地址X]将更改。
杰夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.