发现问题
首先来看看在列表推导式中使用 yield 会发生什么:
[(yield x) for x in [1,2,3]] # 在列表推导式中使用 yield
>>> <generator object <listcomp> at 0x00C21510>
相当奇怪,返回一个 <listcomp>
类型的生成器而不是一个包含生成器的列表
既然是生成器,那我们就用 list()
展开看一下:
list([(yield x) for x in [1,2,3]])
>>> [1, 2, 3]
虽然打印出了预期的结果,但还是感觉很奇怪:
列表推导式本身并不需要用 list()
展开的,这里却需要加上才行
那我们来看看如果是生成器表达式会如何:
((yield x) for x in [1,2,3]) # 在生成器表达式中使用 yield
>>> <generator object <genexpr> at 0x00C21C30>
嗯,的确是返回一个 <genexpr>
类型的生成器,这很正常,那我们用 list()
展开看一下:
list((yield x) for x in [1,2,3])
>>> [1, None, 2, None, 3, None]
这下就懵逼了,怎么会多出几个 None
?这几个 None
从哪里来的?
深入分析
先来看看不含 yield 的列表推导式的 bytecode:
from dis import dis
dis("""[x for x in [1,2,3]]""")
>>> 1 0 LOAD_CONST 0 (<code object <listcomp> at 0x054E0548, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_CONST 5 ((1, 2, 3))
8 GET_ITER
10 CALL_FUNCTION 1
12 RETURN_VALUE
可以看到,列表推导式实际上被转换(MAKE_FUNCTION)成了一个函数并调用(CALL_FUNCTION)
同时把 [1,2,3]
和 [1,2,3]
的迭代器作为参数传给该函数
注意该函数的代码作为一个 code object 传给 MAKE_FUNCTION
<listcomp>
告诉 MAKE_FUNCTION 该函数的类型
注意到该函数被包裹在列表推导式中,是一个嵌套的函数
我们来看看该函数实际的 bytecode:
dis(compile("""[x for x in [1,2,3]]""", '', 'exec').co_consts[0])
>>> 1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 8 (to 14)
6 STORE_FAST 1 (x)
8 LOAD_FAST 1 (x)
10 LIST_APPEND 2
12 JUMP_ABSOLUTE 4
>> 14 RETURN_VALUE
翻译成 Python 的伪代码,该函数相当于:
def listcomp(some_iterable):
res = []
for x in some_iterable:
res.append(x)
return res
使用该列表推导式相当于调用该函数:
listcomp([1,2,3])
>>> [1, 2, 3]
下面看看含有 yield 的列表推导式的 bytecode:
dis("""[(yield x) for x in [1,2,3]]""")
>>> 1 0 LOAD_CONST 0 (<code object <listcomp> at 0x054E0A18, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_CONST 5 ((1, 2, 3))
8 GET_ITER
10 CALL_FUNCTION 1
12 RETURN_VALUE
与之前无异,但我们来看看其内部函数的 bytecode:
dis(compile("""[(yield x) for x in [1,2,3]]""", '', 'exec').co_consts[0])
>>> 1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 10 (to 16)
6 STORE_FAST 1 (x)
8 LOAD_FAST 1 (x)
10 YIELD_VALUE
12 LIST_APPEND 2
14 JUMP_ABSOLUTE 4
>> 16 RETURN_VALUE
对比不含 yield 的 bytecode,发现只是多出了 YIELD_VALUE 这一行
但却把该函数转化成了一个生成器函数,相当于:
def listcomp(some_iterable):
res = []
for x in some_iterable:
v = yield x # 关键的一行
res.append(v)
return res
我们试着像之前那样调用 listcomp
:
listcomp([1,2,3])
>>> <generator object listcomp at 0x052AFD50>
果然得到一个类型为 listcomp
的生成器,用 list()
展开:
list(listcomp([1,2,3]))
>>> [1, 2, 3]
现在知道为什么含有 yield 的列表推导式只是返回一个类型为 listcomp
生成器了吧
因为使用含有 yeild 的列表推导式相当于调用上面 listcomp
这个函数
而该函数是一个生成器函数,只会返回一个生成器
需要我们自己使用 list()
去迭代来把它展开
list()
拿到的值正是 yield x
传出来的
而内部的 res 实际上收集的是外界(这里是 list)迭代 send 进去的值
那这个 res 最终哪里去了?别忘了生成器迭代的结尾:return 的返回值作为 StopIteration
的值
from itertools import islice
gen = listcomp([1,2,3])
list(islice(gen, 3)) # 使用 islice 控制调用 next 的次数,避免直接调用 list 到达迭代的末尾而触发 StopIteration
>>> [1, 2, 3]
try:
next(gen) # 手动触发 StopIteration
except StopIteration as e:
print(e.value)
>>> [None, None, None]
三个 None,这就是内部函数的 res 收集外部 list 迭代展开时传进去的值
list 在迭代时就是调用 next,而 next(gen) 相等于 gen.send(None)
那为什么含有 yield 的生成器表达式在迭代展开时会把 None 也传递出来呢?
聪明的你应该会马上想到:该不会 yield 下面还有一个 yield 吧?这样就能把外界传递进来的 None 再传递出去
的确如此,但为什么会有两个yield?要想弄明白这个问题,想来看看不含 yield 的生成器表达式的 bytecode:
dis("""(x for x in [1,2,3])""")
>>> 1 0 LOAD_CONST 0 (<code object <genexpr> at 0x054F13E8, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<genexpr>')
4 MAKE_FUNCTION 0
6 LOAD_CONST 5 ((1, 2, 3))
8 GET_ITER
10 CALL_FUNCTION 1
12 RETURN_VALUE
与之前列表推导式的 bytecode 没什么大的区别,只是函数的类型是 <genexpr>
而不是 listcomp
那我们看看内部函数的 bytecode:
dis(compile("""(x for x in [1,2,3])""", '', 'exec').co_consts[0])
>>> 1 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 10 (to 14)
4 STORE_FAST 1 (x)
6 LOAD_FAST 1 (x)
8 YIELD_VALUE
10 POP_TOP
12 JUMP_ABSOLUTE 2
>> 14 LOAD_CONST 0 (None)
16 RETURN_VALUE
由于本身就是一个生成器表达式,所以不再需要一个内部的 list(没有 BUILD_LIST)来存放每个值
而是直接把值 yield 到外部(YIELD_VALUE,yield 的返回值会覆盖 运行时栈 的栈顶(被 yield 出去的值))
该函数相当于:
def genexpr(some_iterable):
for x in some_iterable:
yield x # 外界传进来的值不用保存了,POP_TOP
genexpr([1,2,3])
>>> <generator object genexpr at 0x054EE420>
list(genexpr([1,2,3]))
>>> [1, 2, 3]
一切都很正常,那我们来看看含 yield 的生成器表达式的 bytecode:
dis("""((yield x) for x in [1,2,3])""")
>>> 1 0 LOAD_CONST 0 (<code object <genexpr> at 0x054F1338, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<genexpr>')
4 MAKE_FUNCTION 0
6 LOAD_CONST 5 ((1, 2, 3))
8 GET_ITER
10 CALL_FUNCTION 1
12 RETURN_VALUE
与不含 yield 的生成器表达式的 bytecode 一样,关键是内部函数的 bytecode:
dis(compile("""((yield x) for x in [1,2,3])""", '', 'exec').co_consts[0])
>>> 1 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 12 (to 16)
4 STORE_FAST 1 (x)
6 LOAD_FAST 1 (x)
8 YIELD_VALUE
10 YIELD_VALUE
12 POP_TOP
14 JUMP_ABSOLUTE 2
>> 16 LOAD_CONST 0 (None)
18 RETURN_VALUE
的确有两个 YIELD_VALUE!对比不含 yield 的生成器表达式内部函数的 bytecode,
发现就只是多了一行 YIELD_VALUE(第一个 YIELD_VALUE)
没错,正是 (yield x)
造成的,这使得内部函数有两个连续的 YIELD_VALUE
外界传进来的值(这里是None)来不及 POP_TOP 就被 YIELD_VALUE 传递出去了!
最终外界的 list 会拿到自己传进去的None,这样相当于:
def genexpr(some_iterable):
for x in some_iterable:
v = yield x
yield v
list(genexpr([1,2,3]))
>>> [1, None, 2, None, 3, None]
含有 yield 的集合推导式和字典推导式也会像列表推导式那样:
{(yield i) for i in range(3)} # 内部函数的类型是 <setcomp>
>>> <generator object <setcomp> at 0x054F8D80>
set({(yield i) for i in range(3)})
>>> {0, 1, 2}
{(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()} # 内部函数的类型是 <dictcomp>
>>> <generator object <dictcomp> at 0x052AF720>
list({(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()})
>>> ['bar', 'foo', 'eggs', 'spam']
举一反三
明白是怎么回事后,我们可以改变外界传进去值,以此改变内部的 list:
def listcomp(some_iterable):
res = [] # 内部list
for x in some_iterable:
v = yield x
res.append(v)
return res
gen = listcomp([1,2,3])
next(gen)
>>> 1
gen.send('bar')
>>> 2
gen.send('bar')
>>> 3
gen.send('bar')
>>> StopIteration Traceback (most recent call last)
<ipython-input-50-ca4a3b892529> in <module>()
----> 1 gen.send('bar')
StopIteration: ['bar', 'bar', 'bar']
注意到内部 list 在 StopIteration 中即为我们传递进去的3个 ‘bar’
生成器表达式也可以这么玩:
def genexpr(some_iterable):
for x in some_iterable:
v = yield x
yield v
gen = genexpr([1,2,3])
next(gen)
>>> 1
gen.send('bar') # 返回'bar'而不是2
>>> 'bar'
gen.send('bar') # 第二次传进去的值并不会被存起来(POP_TOP)
>>> 2
gen.send('foo')
>>> 'foo'
gen.send('foo')
>>> 3
gen.send('root')
>>> 'root'
gen.send('root')
>>> StopIteration Traceback (most recent call last)
<ipython-input-58-38d221a6c3cc> in <module>()
----> 1 gen.send('root')
StopIteration:
注意最后 StopIteration 的值其实为 None
因为内部函数 genexpr 最终返回的就是 None 而不像 listcomp 那样返回一个 list
上面都是在自己模拟的函数实现的,如何在真正的列表推导式和生成器表达式上向内部传递值呢?
list()
迭代展开只是调用 next,我们无法 send 自己的值进去,所以我们可以在推导式或表达式本身动手脚:
def bar(x):
return 'bar'
gen = [bar((yield x)) for x in [1,2,3]]
list(islice(gen , 3))
>>> [1, 2, 3]
try:
next(gen)
except StopIteration as e:
print(e.value)
>>> ['bar', 'bar', 'bar']
使用一个自定义的函数”传”进去就可以了
这里并不是真正的 send,只是利用自定义的函数“偷梁换柱”罢了(相当于装饰器)
dis(compile("""[(lambda v:'bar')((yield x)) for x in [1,2,3]]""",'','exec').co_consts[0])
>>> 1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 18 (to 24)
6 STORE_FAST 1 (x)
8 LOAD_CONST 0 (<code object <lambda> at 0x00C76498, file "", line 1>)
10 LOAD_CONST 1 ('<listcomp>.<lambda>')
12 MAKE_FUNCTION 0
14 LOAD_FAST 1 (x)
16 YIELD_VALUE
18 CALL_FUNCTION 1
20 LIST_APPEND 2
22 JUMP_ABSOLUTE 4
>> 24 RETURN_VALUE
(yield x)
的返回值(这里是None)作为参数传递给该函数
而该函数执行返回的结果才是真正 append 到内部 list 的值(这里是‘bar’)
我们可以在自定义的函数中打印出外界传进来的值:
def bar(x):
print(x)
return 'bar'
gen = [bar((yield x)) for x in [1,2,3]]
next(gen)
>>> 1
next(gen)
>>> None
>>> 2
next(gen)
>>> None
>>> 3
next(gen)
>>> None
>>> StopIteration Traceback (most recent call last)
<ipython-input-71-8a6233884a6c> in <module>()
----> 1 next(gen)
StopIteration: ['bar', 'bar', 'bar']
下面对生成器表达式也使用自定义的函数 “传递” 值:
def bar(x):
print(x)
return 'bar'
list(bar((yield x)) for x in [1,2,3])
>>> None
>>> None
>>> None
>>> [1, 'bar', 2, 'bar', 3, 'bar']
到这里我想说的是,我们不应该在列表(集合、字典)推导式或生成器表达式中使用 yield,有时会带来意想不到的结果
这其实是 Python 本身的一个 Bug,见 https://bugs.python.org/issue10544
该 Bug 在 Python3.7 中会提示一个 DeprecationWarning,在 Python3.8 中则是 SyntaxError