30.4.7. 通过yield from把多个生成器连起来用

生成器有很多好处,能解决常见的问题。而且可以一个连着一个地用。

例如,我们要编写一个图形程序,让它在屏幕上移动图像,从而形成动画效果。假设要实现这样一段动画:图片先快速移动一段时间,然后暂停,接下来慢速移动一段时间。 为了把移动与暂停表示出来,笔者定义了下面两个生成器函数,让它们分别给出图片在当前时间段内应该保持的速度。

def move(period, speed):
    for _ in range(period):
        yield speed


def pause(delay):
    for _ in range(delay):
        yield 0

为了制作动画,需要将move与pause连起来用,从而算出这张图片当前的位置与上一个位置之差。 下面的函数用三个for循环来表示动画的三个环节,在每个环节里,它都通过yield把图片当前的位置与上一次的位置之差delta返回给调用者。 根据animate函数返回的delta值,即可把整段动画做好。

def animate():
    for delta in move(4, 5.0):
        yield delta
    for delta in pause(3):
        yield delta
    for delta in move(2, 3.0):
        yield delta

接下来,我们就根据animate生成器所给出的delta值,把整个动画效果渲染出来。

def render(delta):
    print(f"Delta: {delta:.1f}")

def run(func):
    for delta in func():
        render(delta)

run(animate)
"""
Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 0.0
Delta: 0.0
Delta: 0.0
Delta: 3.0
Delta: 3.0
"""

这种写法的问题在于,animate函数里有很多重复的地方。比如它反复使用for结构来操纵生成器,而且每个for结构都使用相同的yield表达式,这样看上去很啰唆。

这个例子仅仅连用了三个生成器,就让代码变得如此烦琐,若是动画里面有十几或几十个环节,那么代码读起来会更加困难。

为了解决这个问题,我们可以改用yield from形式的表达式来实现。这种形式,会先从嵌套进去的小生成器里面取值,如果该生成器已经用完,那么程序的控制流程就会回到yield from所在的这个函数之中,然后它有可能进入下一套yield from逻辑。

下面这段代码,用yield from语句重新实现了animate函数。

def animate_composed():
    yield from move(4, 5.0)
    yield from pause(3)
    yield from move(2, 3.0)

run(animate_composed)

它的运行结果于刚才一样,但是代码看上去更清晰、更直观了。而且这种实现方式要更快。

import timeit

def child():
    for i in range(1_000_000):
        yield i

def slow():
    for i in child():
        yield i

def fast():
    yield from child()

baseline = timeit.timeit(stmt='for _ in slow(): pass', globals=globals(),number=50)
print(f'Manual nesting {baseline:.2f}s')

comparison = timeit.timeit( stmt='for _ in fast(): pass',globals=globals(),number=50)
print(f'Composed nesting {comparison:.2f}s')

reduction = -(comparison - baseline) / baseline
print(f'{reduction:.1%} less time')

"""
Manual nesting 6.24s
Composed nesting 5.42s
13.2% less time
"""

所以,如果要把多个生成器连起来用,那么强烈建议优先考虑yield from表达式。

要点:

如果要连续使用多个生成器,那么可以通过yield from表达式来分别使用这些生成器,这样做能够免去重复的for结构。

yield from的性能要胜过那种在for循环里手工编写yield表达式的方案