lambda 函数闭包捕获啥?

Posted

技术标签:

【中文标题】lambda 函数闭包捕获啥?【英文标题】:What do lambda function closures capture?lambda 函数闭包捕获什么? 【发布时间】:2010-02-19 09:46:31 【问题描述】:

最近我开始使用 Python,并且发现了闭包工作方式中的一些特殊情况。考虑以下代码:

adders=[None, None, None, None]

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

print adders[1](3)

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

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

这让我想知道以下几点:

闭包究竟捕获了什么? 说服lambda 函数以在i 更改其值时不会受到影响的方式捕获i 的当前值的最优雅方法是什么?

【问题讨论】:

我在 UI 代码中遇到过这个问题。把我逼疯了。诀窍是记住循环不会创建新范围。 @TimMB i 如何离开命名空间? @detly 好吧,我想说print i 在循环后不起作用。但我为自己测试了它,现在我明白你的意思了——它确实有效。我不知道循环变量在python中的循环体之后徘徊。 这在官方 Python 常见问题解答中,Why do lambdas defined in a loop with different values all return the same result? 下,有解释和通常的解决方法。 @abarnert:所以在 C++ 中,带有 [&] 的 lambda 是闭包(尽管生命周期有限),而带有 [=] 的 lambda 不是闭包?这种定义的选择不会让任何人感到困惑 ;-) 在nonlocal 之前的 Python 中,赋值 没有 对嵌套函数中捕获的变量起作用,所以它们是闭包还是被读取- 仅通过引用足以成为闭包的词法范围进行绑定?我想知道以这种方式将 lambda 演算应用于命令式语言是否实际上是在浪费精力,最好是发明新的术语...... 【参考方案1】:

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

>>> 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 用于使用默认值。在定义 lambda 时进行评估使它们非常适合这种用途。 +1 也是因为这是the official FAQ 认可的解决方案。 这太棒了。然而,默认的 Python 行为不是。 虽然这似乎不是一个好的解决方案......您实际上正在更改函数签名只是为了捕获变量的副本。而且那些调用函数的人也可能会弄乱 i 变量,对吧? @DavidCallanan 我们谈论的是 lambda:一种您通常在自己的代码中定义的临时函数来堵住漏洞,而不是您通过整个 sdk 共享的东西。如果你需要一个更强的签名,你应该使用一个真正的函数。【参考方案2】:

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

闭包到底捕获了什么?

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

关于如何克服这个问题的其他问题,我想到了两种方法:

    最简洁但不是严格等效的方法是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)]

【讨论】:

Python 有静态作用域,而不是动态作用域。只是所有变量都是引用,所以当您将变量设置为新对象时,变量本身(引用)具有相同的位置,但它指向别的东西。如果你set!,Scheme 中也会发生同样的事情。请参阅此处了解真正的动态范围:voidspace.org.uk/python/articles/code_blocks.shtml 选项 2 类似于函数式语言所称的“柯里化函数”。【参考方案3】:

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

【讨论】:

随着岁月的流逝,我越来越相信这是解决问题的最佳方法。【参考方案4】:

考虑以下代码:

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

【讨论】:

这是循环,因为在许多其他语言中,循环可以创建新范围。 这个答案很好,因为它解释了为什么每个 lambda 函数都访问相同的 i 变量。【参考方案5】:

这是一个新示例,它突出显示了闭包的数据结构和内容,以帮助阐明何时“保存”封闭上下文。

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

【讨论】:

NB 输出int object at [address X]&gt; 让我认为闭包正在存储 [address X] AKA 引用。但是,如果在 lambda 语句之后重新分配变量,[address X] 将会改变。【参考方案6】:

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

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)

【讨论】:

克里斯,当然上面的代码和我原来的问题无关。它的构建以一种简单的方式说明我的观点。这当然是毫无意义和愚蠢的。【参考方案7】:

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

【讨论】:

以上是关于lambda 函数闭包捕获啥?的主要内容,如果未能解决你的问题,请参考以下文章

C++ lambda表达式(函数指针和function)

Swift:闭包(Closures)

Swift:闭包

Swift学习笔记-继续学习闭包

Swift学习笔记-继续学习闭包

了解 Lambda 闭包类型如何删除默认构造函数