嵌套生成器表达式 - 意外结果

1 投票
1 回答
506 浏览
提问于 2025-04-18 00:11

这是测试代码:

units = [1, 2]
tens = [10, 20]
nums = (a + b for a in units for b in tens)
units = [3, 4]
tens = [30, 40]
[x for x in nums]

假设第3行的生成器表达式(nums = ...)形成了一个迭代器,我本来期待最后的结果能反映unitstens的最终赋值。另一方面,如果这个生成器表达式在第3行被计算,产生一个结果元组,那么我就期待使用unitstens的第一次定义。

但我看到的是一种混合的情况;也就是说,结果是[31, 41, 32, 42]!?

有没有人能解释一下这种行为?

1 个回答

3

生成器表达式可以看作是一种“函数”,它只有一个参数,就是最外层的可迭代对象。

在这里,这个可迭代对象是 units,当生成器表达式被创建时,它就被当作参数绑定了。

其他的名字要么是局部变量(比如 ab),要么是全局变量,或者是闭包。tens 是作为全局变量查找的,所以每次你推进生成器的时候,它都会被查找。

因此,units 在第3行被绑定到生成器上,而 tens 则是在你最后一行迭代生成器表达式时被查找的。

你可以通过将生成器编译成字节码并检查这个字节码来看到这一点:

>>> import dis
>>> genexp_bytecode = compile('(a + b for a in units for b in tens)', '<file>', 'single')
>>> dis.dis(genexp_bytecode)
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x10f013ae0, file "<file>", line 1>)
              3 LOAD_CONST               1 ('<genexpr>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (units)
             12 GET_ITER
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 PRINT_EXPR
             17 LOAD_CONST               2 (None)
             20 RETURN_VALUE

MAKE_FUNCTION 字节码将生成器表达式的代码对象变成了一个函数,并且这个函数会立即被调用,传入 iter(units) 作为参数。在这里,tens 这个名字根本没有被引用。

这在 原始生成器 PEP 中有说明:

只有最外层的 for 表达式会立即被计算,其他的表达式会被延迟到生成器运行时再计算:

g = (tgtexp  for var1 in exp1 if exp2 for var2 in exp3 if exp4)

这相当于:

def __gen(bound_exp):
    for var1 in bound_exp:
        if exp2:
            for var2 in exp3:
                if exp4:
                    yield tgtexp
g = __gen(iter(exp1))
del __gen

生成器表达式参考 中也有说明:

在生成器表达式中使用的变量会在调用生成器对象的 __next__() 方法时被延迟计算(和普通生成器的方式一样)。不过,最左边的 for 子句会立即被计算,这样可以在处理生成器表达式的代码中,先看到它可能产生的错误,而不是其他可能的错误。后续的 for 子句不能立即计算,因为它们可能依赖于前一个循环。例如:(x*y for x in range(10) for y in bar(x))

这个 PEP 中有一段很好的说明,解释了为什么(除了最外层的可迭代对象)其他名字会被延迟绑定,具体可以查看 早绑定与晚绑定

撰写回答