带有生成器的Python闭包

Foo*_*Bar 6 python python-3.x

def multipliers():
  return [lambda x : i * x for i in range(4)]

print [m(2) for m in multipliers()]
Run Code Online (Sandbox Code Playgroud)

我部分理解(很危险)i所有功能都相同的原因,因为Python的闭包是后期绑定。

输出是[6, 6, 6, 6](不像[0, 2, 4, 6]我期望的那样)。


我看到它可以与生成器一起正常工作,我的预期输出来自以下版本。

def multipliers():
  return (lambda x : i * x for i in range(4))

print [m(2) for m in multipliers()]
Run Code Online (Sandbox Code Playgroud)

任何简单的解释为什么它可以在以下版本中使用?

Dun*_*can 6

It only works because you call each function before the next one is created. The generator is lazy, it yields each function immediately, so before i is incremented. Compare if you force all of the generator to be consumed before you call the functions:

>>> def multipliers():
...   return (lambda x : i * x for i in range(4))
...
>>> print [m(2) for m in multipliers()]
[0, 2, 4, 6]
>>> print [m(2) for m in list(multipliers())]
[6, 6, 6, 6]
Run Code Online (Sandbox Code Playgroud)

If you want early binding then you can simulate it here with default arguments:

>>> def multipliers():
...   return (lambda x, i=i : i * x for i in range(4))
...
>>> print [m(2) for m in multipliers()]
[0, 2, 4, 6]
>>> print [m(2) for m in list(multipliers())]
[0, 2, 4, 6]
Run Code Online (Sandbox Code Playgroud)

To clarify my comment about the generator being lazy: the generator (lambda x : i * x for i in range(4)) will go through values of i from 0 to 3 inclusive, but it yields the first function while i is still 0, at that point it hasn't bothered to do anything about the cases for 1 to 3 (which is why we say it is lazy).

The list comprehension [m(2) for m in multipliers()] calls the first function m immediately, so i is still 0. Then the next iteration of the loop retrieves another function m where i is now 1. Again the function is called immediately so it sees i as 1. And so on.


hol*_*web 5

您正在寻找对复杂现象的简单解释,但我会尽量保持简短。

第一个函数返回一个函数列表,每个函数都是函数的闭包multipliers。因此,解释器存储对“单元格”的引用,引用i局部变量,允许该值在创建它的函数调用结束后继续存在,并且其局部命名空间已被销毁。

不幸的是,单元格中的引用是函数终止时变量的值,而不是它用于创建 lambda 时的值(因为它在循环中使用了四次,解释器必须创建每次使用都有一个单独的单元格,但它没有)。

您的第二个函数返回一个生成器表达式,它有自己的本地命名空间,i在处理yielded 结果期间暂停时保留本地变量的值(在本例中,特别是)。

您会发现您可以将其显式地重铸为生成器函数,这可能有助于解释第二个示例的操作:

def multipliers():
    for i in range(4):
        yield lambda x : i * x
Run Code Online (Sandbox Code Playgroud)

这也给出了所需的结果。