Python生成器的闭包

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

print [m(2) for m in multipliers()]

我部分地理解了(这是危险的),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()]

为什么在下面的版本中它能够工作,有简单的解释吗?
3个回答

8

它之所以工作,是因为在创建下一个函数之前调用每个函数。生成器是懒惰的,它立即产生每个函数,因此在增加i之前。如果你强制在调用函数之前消耗掉所有生成器,请进行比较:

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

如果您想进行早期绑定,那么可以使用默认参数在此处模拟它:
>>> 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]

为了澄清我的有关生成器懒惰的评论:生成器 (lambda x : i * x for i in range(4)) 将遍历值为0到3的i,但在i仍为0时就会产生第一个函数,此时它还没有处理1到3的情况(这就是我们说它是“懒惰”的原因)。
列表推导式 [m(2) for m in multipliers()] 立即调用第一个函数 m,因此 i 仍为0。然后循环的下一次迭代检索另一个函数 m,其中 i 现在为1。再次立即调用该函数,所以它将i 视为1。依此类推。

这再简单不过了:“它之所以能够工作,是因为你在创建下一个函数之前调用了每个函数。” - Santanu Sahoo
我不理解这个。 “生成器是惰性的,它立即产生每个函数” 在表面上似乎矛盾。立即相对于什么? - ivvi
@seron,我扩展了我的回答。基本上它在执行“for i in range(4)”循环的下一次迭代之前立即生成每个函数。 - Duncan

6
你正在寻找一个复杂现象的简单解释,但我会尽量简短地说明。
第一个函数返回一个函数列表,其中每个函数都是对multipliers函数的闭包。因此,解释器存储了一个引用到“单元格”,引用i局部变量,使得值在创建它的函数调用结束后仍然存在,并且其本地命名空间已被销毁。
不幸的是,单元格中的引用是对变量在函数终止时的值的引用,而不是在用于创建lambda时的值(由于它在循环中使用了四次,解释器必须为每个使用创建一个单独的单元格,但它没有这样做)。
你的第二个函数返回一个生成器表达式,它有自己的本地命名空间,在处理yielded结果时保留本地变量的值(在这种情况下,特别是i)。
你会发现,你可以将其明确地重构为生成器函数,这可能有助于解释第二个示例的操作:
def multipliers():
    for i in range(4):
        yield lambda x : i * x

这也能产生所需的结果。

0

理解这个复杂示例的一些要点:

  • 3个函数的闭包指向make_fns_by_...作用域中相同的i
  • 生成器是“懒惰”的,如下面详细解释的那样——它实际上改变了代码调用顺序
def make_fns_by_list():
    fns = []
    for i in list(range(3)):

        def f():
            print(i)  # ref. to "global var" `i` in closure

        print(id(f), f.__closure__, f.__closure__[0].cell_contents)
        fns.append(f)

    return fns


def make_fns_by_generator():
    for i in list(range(3)):

        def f():
            print(i)  # ref. to "global var" `i` in closure

        print(id(f), f.__closure__, f.__closure__[0].cell_contents)
        yield(f)


def call_fns():

    fns = make_fns_by_generator()  # generator is lazy, do nothing here

    # for f in fns:
    #     print(id(f), f.__closure__, f.__closure__[0].cell_contents)
    # same as below which is easier for explanation:

    fns_iter = iter(fns)
    f = next(fns_iter)  # generator is "lazy", it make `f` here
    print(id(f), f.__closure__, f.__closure__[0].cell_contents, '-->', f())  # and called at once
    f = next(fns_iter)
    print(id(f), f.__closure__, f.__closure__[0].cell_contents, '-->', f())
    f = next(fns_iter)
    print(id(f), f.__closure__, f.__closure__[0].cell_contents, '-->', f())

    print('-' * 100)

    fns = make_fns_by_list()  # list is working hard, it make `f` here

    fns_iter = iter(fns)
    f = next(fns_iter)
    print(id(f), f.__closure__, f.__closure__[0].cell_contents, '-->', f())  # and called at once
    f = next(fns_iter)
    print(id(f), f.__closure__, f.__closure__[0].cell_contents, '-->', f())
    f = next(fns_iter)
    print(id(f), f.__closure__, f.__closure__[0].cell_contents, '-->', f())


def main():
    call_fns()


if __name__ == '__main__':
    main()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接