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

时间:2010-02-19 09:46:31

标签: python lambda closures

最近我开始玩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的当前值?

7 个答案:

答案 0 :(得分:168)

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

>>> 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的值)

答案 1 :(得分:136)

你的第二个问题已得到解答,但至于你的第一个问题:

  

封闭捕获到底是什么?

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

编辑:关于如何克服这个问题的另一个问题,有两种方法可以想到:

  1. 最简洁但不严格等同的方式是one recommended by 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)]
    

答案 2 :(得分:25)

为了完整性,您对第二个问题的另一个答案是:您可以在partial模块中使用functools

通过从运营商导入add,Chris Lutz建议示例变为:

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 :(得分:17)

请考虑以下代码:

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 :(得分: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)

答案 5 :(得分: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的闭包中,并且我不在f_3的闭包中(未显示),这表明闭包对象本身就是不同的对象。

闭包对象本身是同一个对象吗?

>>> print f_1.func_closure is f_2.func_closure
False

答案 6 :(得分:0)

整理 i 的作用域的一种方法是在另一个作用域(一个闭包函数)中生成 lambda,为其传递必要的参数以生成 lambda:

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