lambda 函数闭包捕获什么?

What do lambda function closures capture?

提问人:Boaz 提问时间:2/19/2010 最后编辑:Karl KnechtelBoaz 更新时间:6/24/2023 访问量:82126

问:

最近我开始玩 Python,我发现闭包的工作方式很奇特。请考虑以下代码:

adders=[None, None, None, None]

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

print adders[1](3)

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

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

这让我想知道以下几点:

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

有关该问题的更易于理解的实用版本,特定于使用循环(或列表推导式、生成器表达式等)的情况,请参阅在循环(或推导式)中创建函数(或 lambda)。这个问题的重点是理解 Python 中代码的底层行为。

如果你来到这里试图解决在 Tkinter 中制作按钮的问题,请尝试在 for 循环中创建按钮,传递命令参数以获得更具体的建议。

有关 Python 如何实现闭包的技术细节,请参阅obj.__closure__中究竟包含哪些内容?有关相关术语的讨论,请参阅早期绑定和晚期绑定之间有什么区别?

Python Lambda 闭包

评论

54赞 detly 6/24/2010
我在UI代码中遇到了这个问题。把我逼疯了。诀窍是要记住,循环不会创建新的范围。
4赞 detly 5/24/2013
@TimMB 如何离开命名空间?i
4赞 Tim MB 5/25/2013
@detly好吧,我想说的是,在循环之后就行不通了。但是我自己测试了它,现在我明白你的意思了——它确实有效。我不知道循环变量在 python 中的循环体之后徘徊。print i
30赞 abarnert 11/6/2014
这在官方的 Python FAQ 中,位于为什么在具有不同值的循环中定义的 lambda 都返回相同的结果?下,并提供了解释和通常的解决方法。
2赞 abarnert 6/12/2015
@SteveJessop:请参阅页面下方的词汇环境的第一段,该段落解释了在命令式语言中,闭包必须是“通过引用”。Python 使这有点令人困惑,因为它具有可变值,这些值具有自己固有的内存位置,但它也具有赋值语句,这些赋值语句不会改变值,而是会改变环境——但假设您希望赋值语句起作用,那么重要的“位置”是名称,而不是内存中的位置。

答:

2赞 Chris Lutz 2/19/2010 #1

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

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

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

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赞 Boaz 2/19/2010
Chris,当然上面的代码与我原来的问题无关。它的构造是为了以一种简单的方式说明我的观点。这当然是毫无意义和愚蠢的。
226赞 Max Shawabkeh 2/19/2010 #2

闭合究竟捕获了什么?

Python 中的闭包使用词法范围:它们记住创建它的闭包变量的名称和范围。但是,它们仍然是后期绑定:在使用闭包中的代码时查找名称,而不是在创建闭包查找名称。由于示例中的所有函数都是在相同的范围内创建的,并且使用相同的变量名称,因此它们始终引用相同的变量。

至少有两种方法可以改为获得早期绑定:

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

  2. 我们可以更详细但更健壮地为每个创建的 lambda 创建一个新范围:

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

评论

4赞 Claudiu 6/29/2010
Python 具有静态作用域,而不是动态作用域。只是所有变量都是引用,所以当你将变量设置为一个新对象时,变量本身(引用)具有相同的位置,但它指向其他东西。同样的事情发生在 Scheme 中,如果你.请参阅此处,了解动态范围的真正含义:voidspace.org.uk/python/articles/code_blocks.shtmlset!
10赞 Crashworks 9/20/2011
选项 2 类似于函数式语言所称的“Curried 函数”。
1赞 Sohail Si 5/23/2022
解决方案 2 更好。我更喜欢它而不是默认参数。它更合乎逻辑,对 Python 设计的具体方式的依赖性更低。第二个 lambda 提供类似于闭包的局部变量。👍 👍
0赞 Karl Knechtel 8/19/2022
从学究上讲,闭包是一种实现技术,它使词法范围能够在函数是第一类对象的语言(例如 Python)中使用后期绑定。动态作用域语言没有或不需要它们,因为它们可以执行与以往相同的动态作用域解析 - 通过向后处理调用堆栈来查找名称。
318赞 Adrien Plisson 2/19/2010 #3

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

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

这个想法是声明一个参数(巧妙地命名)并为其提供要捕获的变量的默认值(值ii)

评论

11赞 quornian 11/14/2012
+1 表示使用默认值。在定义 lambda 时进行评估使它们非常适合此用途。
40赞 abarnert 11/6/2014
+1 也因为这是官方常见问题解答认可的解决方案。
67赞 Cecil Curry 8/3/2016
这真是太神奇了。但是,默认的 Python 行为并非如此。
6赞 David Callanan 1/12/2020
不过,这似乎不是一个好的解决方案......您实际上只是为了捕获变量的副本而更改函数签名。而且那些调用该函数的人可能会弄乱 i 变量,对吧?
6赞 Adrien Plisson 1/14/2020
@DavidCallanan,我们谈论的是 lambda:一种临时函数,您通常在自己的代码中定义以填补漏洞,而不是通过整个 SDK 共享的内容。如果你需要更强的签名,你应该使用一个真正的函数。
33赞 mthurlin 2/19/2010 #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

评论

27赞 detly 6/24/2010
这是循环,因为在许多其他语言中,循环可以创建一个新范围。
2赞 David Callanan 8/15/2018
这个答案很好,因为它解释了为什么为每个 lambda 函数访问相同的变量。i
0赞 Karl Knechtel 8/19/2022
我不认为混淆是因为循环,因为即使使用新的范围,该值仍然会发生变化。我认为混淆是因为 - 相反,因为为在封闭范围内查找的名称创建了闭包。因为 Python 的函数是第一类对象,所以很容易有一种直觉,即它应该在创建时“知道”它需要的一切——比如当我们实例化一个类时,如何为属性赋值,并作为早期绑定的命名空间运行。lambdalambdalambda__init__selfself
0赞 Karl Knechtel 8/19/2022
当然,在这里的例子中,我们看到了一个缺陷,即全局的后期查找所说明的缺陷。但是,当我在几年前第一次遇到这个问题时,我认为我预计全局命名空间是一个特例,并且本地命名空间和封闭命名空间不会以这种方式运行。毕竟,可以离开这些范围,对吧?所以你必须尽早绑定以避免这个问题,对吧?如果不给出,如果我们在其他地方使用它们,为什么不为超出范围的 NameError 引发 NameError?答案当然是关闭。adders[1](3)4ireturnadders
52赞 Joma 4/11/2012 #5

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

评论

1赞 Karl Knechtel 9/28/2021
随着岁月的流逝,我越来越相信这是解决问题的最佳方法。
6赞 Jeff 5/9/2014 #6

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

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

评论

1赞 Jeff 5/14/2014
注意:输出让我认为闭包正在存储 [地址 X] AKA 引用。但是,如果在 lambda 语句之后重新分配变量,则 [address X] 将更改。int object at [address X]>
-1赞 Joffan 5/26/2021 #7

整理作用域的一种方法是在另一个作用域(闭包函数)中生成 lambda,并交出必要的参数来生成 lambda:i

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

评论

0赞 Karl Knechtel 8/19/2022
已经有多个答案显示了这种技术。我不明白这个答案应该添加什么。
0赞 Joffan 8/20/2022
使用这个没有多个答案。在仔细查看其他答案时,我看到它在 Mark Shawabkeh 的答案末尾提到过。
0赞 mhrsalehi 6/24/2023 #8

在函数中创建加法器以捕获值:

def create_adder(i):
    return lambda a: i + a


if __name__ == '__main__':
    adders = [None, None, None, None]

    for i in [0, 1, 2, 3]:
        adders[i] = create_adder(i)

    print(adders[1](3))