不要在列表(集合、字典)推导式或生成器表达式中使用 yield

发现问题

首先来看看在列表推导式中使用 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

参考资料

  1. yield-in-list-comprehensions-and-generator-expressions
  2. issue10544
  3. wtfpython-yielding None
文章目录
  1. 1. 发现问题
  2. 2. 深入分析
  3. 3. 举一反三
  4. 4. 参考资料
|