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

loading...


196

最近我开始玩Python,我遇到了一些特殊的闭包方式 . 请考虑以下代码:

adders=[0,1,2,3]

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

print adders[1](3)

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

我期待 4 . 我的理由是:在Python中,一切都是一个对象,因此每个变量都是指向它的指针 . 为 i 创建 lambda 闭包时,我希望它存储一个指向 i 当前指向的整数对象的指针 . 这意味着当 i 分配了一个新的整数对象时,它不应该影响先前创建的闭包 . 遗憾的是,检查调试器中的 adders 数组表明它确实存在 . 所有 lambda 函数都引用 i3 的最后一个值,这会导致 adders[1](3) 返回 6 .

这让我想知道以下内容:

  • 闭包捕获的内容是什么?

  • 最优雅的方法是说服 lambda 函数以 i 更改其值时不会受到影响的方式捕获 i 的当前值?

6回答

  • 126

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

    闭包捕获到底是什么?

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

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

    • 最简洁但不严格等同的方式是one recommended by Adrien Plisson . 使用额外参数创建lambda,并将额外参数的默认值设置为要保留的对象 .

    • 每次创建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)]
    

  • 154

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

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


  • 16

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

    通过从运营商导入添加,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)
    

  • 24

    请考虑以下代码:

    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
    

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

  • 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
    

loading...

评论

暂时没有评论!