lambda 函数闭包捕获什么?

发布于 2024-08-22 03:41:39 字数 1494 浏览 4 评论 0原文

最近,我开始使用 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 的当前值的最优雅的方法是什么它的价值?

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

如果您尝试解决在 Tkinter 中制作按钮的问题,请尝试 tkinter 在 for 循环中创建按钮并传递命令参数以获取更具体的建议。

请参阅 obj.__closure__ 中到底包含什么? 了解 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)

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.

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:

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

For a more accessible, practical version of the question, specific to the case where a loop (or list comprehension, generator expression etc.) is used, see Creating functions (or lambdas) in a loop (or comprehension). This question is focused on understanding the underlying behaviour of the code in Python.

If you got here trying to fix a problem with making buttons in Tkinter, try tkinter creating buttons in for loop passing command arguments for more specific advice.

See What exactly is contained within a obj.__closure__? for technical details of how Python implements closures. See What is the difference between Early and Late Binding? for related terminology discussion.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

烂人 2024-08-29 03:41:39

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

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

you may force the capture of a variable using an argument with a default value:

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

the idea is to declare a parameter (cleverly named i) and give it a default value of the variable you want to capture (the value of i)

审判长 2024-08-29 03:41:39

闭包到底捕获了什么?

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

至少有两种方法可以实现早期绑定:

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


  2. 更详细但也更稳健的是,我们可以为每个创建的 lambda 创建一个新作用域:

    <前><代码>>>>加法器 = [0,1,2,3]
    >>>>>对于 [0,1,2,3] 中的 i:
    ... adders[i] = (lambda b: lambda a: b + a)(i)
    ...
    >>>>>加法器[1](3)
    4
    >>>>>加法器[2](3)
    5

    这里的作用域是使用一个新函数(为了简洁起见,另一个 lambda)创建的,该函数绑定其参数,并传递您想要绑定的值作为参数。不过,在实际代码中,您很可能会使用普通函数而不是 lambda 来创建新作用域:

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

What do the closures capture exactly?

Closures in Python use lexical scoping: they remember the name and scope of the closed-over variable where it is created. However, they are still late binding: the name is looked up when the code in the closure is used, not when the closure is created. 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.

There are at least two ways to get early binding instead:

  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.

  2. More verbosely but also more robustly, we can create a new scope for each created 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
    

    The scope here is created using a new function (another 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)]
    
掌心的温暖 2024-08-29 03:41:39

为了完整起见,您的第二个问题的另一个答案:您可以在functools 模块。

按照 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)

For completeness another answer to your second question: You could use partial in the functools module.

With importing add from operator as Chris Lutz proposed the example becomes:

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)
空城仅有旧梦在 2024-08-29 03:41:39

考虑以下代码:

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

Consider the following code:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

I think most people won't find this confusing at all. It is the expected behaviour.

So, why do people think it would be different when it is done in a loop? I know I did that mistake myself, but I don't know why. It is the loop? Or perhaps the lambda?

After all, the loop is just a shorter version of:

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
因为看清所以看轻 2024-08-29 03:41:39

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

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

Here's a new example that highlights the data structure and contents of a closure, to help clarify when the enclosing context is "saved."

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

What is in a closure?

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

Notably, my_str is not in f1's closure.

What's in f2's closure?

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

Notice (from the memory addresses) that both closures contain the same objects. So, you can start to think of the lambda function as having a reference to the scope. However, my_str is not in the closure for f_1 or f_2, and i is not in the closure for f_3 (not shown), which suggests the closure objects themselves are distinct objects.

Are the closure objects themselves the same object?

>>> print f_1.func_closure is f_2.func_closure
False
不如归去 2024-08-29 03:41:39

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

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)

In answer to your second question, the most elegant way to do this would be to use a function that takes two parameters instead of an array:

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

However, using lambda here is a bit silly. Python gives us the operator module, which provides a functional interface to the basic operators. The lambda above has unnecessary overhead just to call the addition operator:

from operator import add
add(1, 3)

I understand that you're playing around, trying to explore the language, but I can't imagine a situation I would use an array of functions where Python's scoping weirdness would get in the way.

If you wanted, you could write a small class that uses your array-indexing syntax:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)
霊感 2024-08-29 03:41:39

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

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

create adder in a function to capture the value:

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))
坚持沉默 2024-08-29 03:41:39

整理 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当然。

One way to sort out the scope of i is to generate the lambda in another scope (a closure function), handing over the necessary parameters for it to make the 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))

giving 5 6 7 8 of course.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文