注意:这是CPython在理解推导式和生成器表达式中的yield时存在的一个错误,在Python 3.8中已得到修复,并在Python 3.7中发出了弃用警告。请参阅
Python bug report和
Python 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中使用
yield
和
yield 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))
[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
作为生成器表达式的一部分,期望是yield
应用于生成器函数,而不是嵌套范围的生成器表达式。 - Martijn Pietersyield
和return
的函数应该如文档所述成为一个生成器函数,其return
值应该落在StopIteration
异常中,而带有yield
的列表推导式的字节码看起来(尽管不是有意的)就像这样的函数的字节码。 - zabolekaryield
表达式,因此将当前代码对象标记为生成器。瞧,我们有了一个生成器函数。 - Martijn Pieters