lambda 函数闭包捕获了什么?

2022-01-29 00:00:00 python lambda closures

问题描述

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

Recently I started playing around with Python and I came around something peculiar in the way closures work. Consider the following code:

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.

It builds a simple array of functions that take a single input and return that input added by a number. The functions are constructed in for loop where the iterator i runs from 0 to 3. For each of these numbers a lambda function is created which captures i and adds it to the function's input. The last line calls the second lambda function with 3 as a parameter. To my surprise the output was 6.

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

I expected a 4. My reasoning was: in Python everything is an object and thus every variable is essential a pointer to it. When creating the lambda closures for i, I expected it to store a pointer to the integer object currently pointed to by i. That means that when i assigned a new integer object it shouldn't effect the previously created closures. Sadly, inspecting the adders array within a debugger shows that it does. All lambda functions refer to the last value of i, 3, which results in adders[1](3) returning 6.

这让我想知道以下几点:

Which make me wonder about the following:

  • 闭包究竟捕获了什么?
  • 说服 lambda 函数以在 i 改变它的值?
  • What do the closures capture exactly?
  • What is the most elegant way to convince the lambda functions to capture the current value of i in a way that will not be affected when i changes its value?

解决方案

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

Your second question has been answered, but as for your first:

闭包到底捕获了什么?

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

Scoping in Python is dynamic and lexical. A closure will always remember the name and scope of the variable, not the object it's pointing to. Since all the functions in your example are created in the same scope and use the same variable name, they always refer to the same variable.

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

Regarding your other question of how to overcome this, there are two ways that come to mind:

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

  1. The most concise, but not strictly equivalent way is the one recommended by Adrien Plisson. Create a lambda with an extra argument, and set the extra argument's default value to the object you want preserved.

每次创建 lambda 时都创建一个新作用域,这样会更冗长但不那么 hacky:

A little more verbose but less hacky would be to create a new scope each time you create the 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 来创建新作用域:

The scope here is created using a new function (a lambda, for brevity), which binds its argument, and passing the value you want to bind as the argument. In real code, though, you most likely will have an ordinary function instead of the lambda to create the new scope:

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

相关文章