关于python:什么是(lambda)函数闭包捕获?

What do (lambda) function closures capture?

最近我开始使用python,在闭包的工作方式中我遇到了一些特殊的问题。请考虑以下代码:

1
2
3
4
5
6
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中,一切都是一个对象,因此每个变量都是指向它的指针。在为i创建lambda闭包时,我希望它存储一个指向当前i所指向整数对象的指针。这意味着当i分配一个新的整数对象时,它不应该影响先前创建的闭包。遗憾的是,在调试器中检查adders数组表明确实如此。所有lambda函数均为i3的最后一个值,导致adders[1](3)返回6

这让我想知道以下几点:

  • 闭包到底捕获了什么?
  • 说服lambda函数捕捉i的当前值的最优雅的方法是什么?这种方法在i更改值时不会受到影响?


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

1
2
3
4
5
>>> 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的值)。


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

what does the closure capture exactly?

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

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

  • 最简洁但并非严格等效的方法是阿德里安·普里松推荐的方法。用一个额外参数创建一个lambda,并将这个额外参数的默认值设置为要保留的对象。

  • 在每次创建lambda时,创建一个新的作用域会更加详细,但不那么容易被黑客攻击:

    1
    2
    3
    4
    5
    6
    7
    8
    >>> 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来创建新的作用域:

    1
    2
    3
    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]

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

    根据Chris Lutz提出的从操作符导入add,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)


    请考虑以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    x ="foo"

    def print_x():
        print x

    x ="bar"

    print_x() # Outputs"bar"

    我想大多数人根本不会觉得这令人困惑。这是预期的行为。

    那么,为什么人们认为当它在一个循环中完成时会有所不同呢?我知道我自己犯了那个错误,但我不知道为什么。是环路吗?或者是lambda?

    毕竟,循环只是一个较短的版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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


    下面是一个新的示例,它突出显示了闭包的数据结构和内容,以帮助澄清何时"保存"了封闭上下文。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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()

    什么是结束语?

    1
    2
    >>> print f_1.func_closure, f_1.func_closure[0].cell_contents
    (<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

    值得注意的是,我的"str"并没有关闭F1。

    二层楼的封闭部分是什么?

    1
    2
    >>> print f_2.func_closure, f_2.func_closure[0].cell_contents
    (<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

    注意(从内存地址)两个闭包都包含相同的对象。因此,可以开始将lambda函数看作是对作用域的引用。但是,我的str不在f_1或f_2的闭包中,我也不在f_3的闭包中(未显示),这表明闭包对象本身是不同的对象。

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

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


    在回答第二个问题时,最优雅的方法是使用一个采用两个参数而不是数组的函数:

    1
    2
    add = lambda a, b: a + b
    add(1, 3)

    然而,在这里使用lambda有点傻。python为我们提供了operator模块,它为基本的操作符提供了一个功能接口。上面的lambda有不必要的开销,只需要调用加法运算符:

    1
    2
    from operator import add
    add(1, 3)

    我知道你在玩弄,试图探索语言,但我无法想象我会使用一系列函数,在这些函数中,python的作用域怪异会妨碍到你。

    如果需要,可以编写一个使用数组索引语法的小类:

    1
    2
    3
    4
    5
    6
    class Adders(object):
        def __getitem__(self, item):
            return lambda a: a + item

    adders = Adders()
    adders[1](3)