列表推导式和生成器表达式中的yield

81

以下行为对我来说似乎相当不符合直觉(Python 3.4):

>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]

最后一行的中间值实际上并不总是 None,它们是我们向生成器 send 的任何内容,相当于以下生成器(我猜):

def f():
   for i in range(3):
      yield (yield i)

我觉得很有趣,这三行代码竟然能够正常工作。根据参考文献yield 只允许在函数定义中使用(虽然我可能理解有误或者它只是从旧版本复制而来)。前两行代码会在Python2.7中产生SyntaxError,但第三行不会。

此外,以下几点也很奇怪:

  • 列表推导返回的是一个生成器而不是一个列表
  • 将生成器表达式转换成列表后,与相应的列表推导包含的值不同。

有没有人能提供更多信息?

1个回答

82
注意:这是CPython在理解推导式和生成器表达式中的yield时存在的一个错误,在Python 3.8中已得到修复,并在Python 3.7中发出了弃用警告。请参阅Python bug reportPython 3.7以及Python 3.8的“新功能”条目。
生成器表达式、集合和字典推导式被编译为(生成器)函数对象。在Python 3中,列表推导式也受到同样的待遇;它们本质上都是一个新的嵌套作用域。
如果您尝试反汇编生成器表达式,就可以看到这一点:
>>> dis.dis(compile("(i for i in range(3))", '', 'exec'))
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
              3 LOAD_CONST               1 ('<genexpr>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (range)
             12 LOAD_CONST               2 (3)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               3 (None)
             26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

上面的内容表明,生成器表达式被编译为代码对象,作为函数加载(MAKE_FUNCTION 从代码对象创建函数对象)。.co_consts[0] 引用让我们看到为表达式所生成的代码对象,并且它使用 YIELD_VALUE 就像生成器函数一样。因此,yield 表达式在这个上下文中起作用,因为编译器将其视为伪装成函数的东西。
这是一个错误;yield 在这些表达式中没有位置。Python 3.7之前的 Python 语法 允许它(这就是为什么代码可编译的原因),但是 yield表达式规范 显示,在这里使用 yield 实际上不应该起作用:

yield 表达式只能在定义 生成器 函数时使用,因此只能在函数定义体中使用。

这已经确认是 问题10544 中的一个错误。该错误的解决方法是,在Python 3.8中使用yieldyield from引发SyntaxError;在Python 3.7中,它引发DeprecationWarning以确保代码停止使用此结构。如果您使用启用Python 3兼容性警告的-3命令行开关,则在Python 2.7.15及以上版本中也会看到同样的警告。
3.7.0b1警告如下; 将警告转换为错误将给出SyntaxError异常,就像在3.8中一样:
>>> [(yield i) for i in range(3)]
<stdin>:1: DeprecationWarning: 'yield' inside list comprehension
<generator object <listcomp> at 0x1092ec7c8>
>>> import warnings
>>> warnings.simplefilter('error')
>>> [(yield i) for i in range(3)]
  File "<stdin>", line 1
SyntaxError: 'yield' inside list comprehension

以下是翻译的结果:
在列表推导式中使用的yield和生成器表达式中使用的yield之间的差异源于这两个表达式实现方式的不同。在Python 3中,列表推导式使用LIST_APPEND调用将堆栈顶部添加到正在构建的列表中,而生成器表达式则会生成该值。添加(yield )只会向任一方添加另一个YIELD_VALUE操作码。
>>> dis.dis(compile("[(yield i) for i in range(3)]", '', 'exec').co_consts[0])
  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                13 (to 22)
              9 STORE_FAST               1 (i)
             12 LOAD_FAST                1 (i)
             15 YIELD_VALUE
             16 LIST_APPEND              2
             19 JUMP_ABSOLUTE            6
        >>   22 RETURN_VALUE
>>> dis.dis(compile("((yield i) for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 YIELD_VALUE
             14 POP_TOP
             15 JUMP_ABSOLUTE            3
        >>   18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

字节码索引为15和12的YIELD_VALUE操作码是多余的,就像巢中的布谷鸟一样。因此,在列表推导转换为生成器时,每次只有1个yield产生堆栈顶部(用yield返回值替换堆栈顶部),而对于生成器表达式变体,则先yield堆栈顶部(整数),然后再yield一次,但现在堆栈包含yield的返回值,第二次得到None
对于列表推导,则仍返回预期的list对象输出,但Python 3将其视为生成器,因此返回值附加到StopIteration异常value属性上:
>>> from itertools import islice
>>> listgen = [(yield i) for i in range(3)]
>>> list(islice(listgen, 3))  # avoid exhausting the generator
[0, 1, 2]
>>> try:
...     next(listgen)
... except StopIteration as si:
...     print(si.value)
... 
[None, None, None]

这些None对象是yield表达式的返回值。
再次强调,这个问题同样适用于Python 2和Python 3中的字典和集合推导式。在Python 2中,yield返回值仍然被添加到预期的字典或集合对象中,并且返回值最后被“yielded”,而不是附加到StopIteration异常。
>>> list({(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()})
['bar', 'foo', 'eggs', 'spam', {None: None}]
>>> list({(yield i) for i in range(3)})
[0, 1, 2, set([None])]

请注意,根据语言规范,“yield-atom”允许在表达式(生成器函数内部)中使用。如果“yield-atom”被错误实现,这可能会更加棘手。 - skyking
2
@skyking:这就是我所说的,语法允许这样做。我所指的错误是尝试在生成器函数内部使用yield作为生成器表达式的一部分,期望是yield应用于生成器函数,而不是嵌套范围的生成器表达式。 - Martijn Pieters
哇,确实非常有用的信息。所以,如果我理解正确的话,以下情况发生了:一个同时包含 yieldreturn 的函数应该如文档所述成为一个生成器函数,其 return 值应该落在 StopIteration 异常中,而带有 yield 的列表推导式的字节码看起来(尽管不是有意的)就像这样的函数的字节码。 - zabolekar
@zabolekar:就像这样;步骤大致如下:编译器遇到列表推导式,因此构建代码对象;编译器遇到yield表达式,因此将当前代码对象标记为生成器。瞧,我们有了一个生成器函数。 - Martijn Pieters
1
@Chris_Rands,2.7的更改是为了在使用“-3”兼容性警告时使用。 - Martijn Pieters
显示剩余6条评论

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