第 33 条 通过 yield from 把多个生成器连起来用
生成器有很多好处(参见第 30 条),而且能够解决许多常见的问题(参见第 31 条)。生成器的用途特别广,所以很多程序都会频繁使用它们,而且是一个连着一个地用。
例如,我们要编写一个图形程序,让它在屏幕上面移动图像,从而形成动画效果。假如要实现这样一段动画:图片先快速移动一段时间,然后暂停,接下来慢速移动一段时间。为了把移动与暂停表示出来,笔者定义了下面两个生成器函数,让它们分别给出图片在当前时间段内应该保持的速度。
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}')
# move the images onscreen
...
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():
yield from move(4, 5.0)
yield from pause(3)
yield from move(2, 3.0)
>>>
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
它的运行结果与刚才一样,但是代码看起来更清晰、更直观了。Python 解释器看到 yield from 形式的表达式后,会自己想办法实现带有普通 yield 语句的 for 循环相同的效果,而且这种实现方式要更快。下面采用内置的 timeit 模块编写并运行一个 mirco-benchmark 试试。
它的运行结果和刚才一样,但是代码看起来更清晰、更直观了。Python 解释器看到 yield from 形式的表达式后,会自己想办法实现与带有普通 yield 语句的 for 循环相同的结果,而且这种实现方式要更快。下面采用内置的 timeit 模块编写并运行一个 micro-benchmark 试试。
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')
comparsion = timeit.timeit(stmt='for _ in fast(): pass',
globals=globals(),
number=50)
print(f'Composed nesting {comparsion:.2f}s')
reduction = -(comparsion - baseline) / baseline
print(f'{reduction:.1%} less time')
所以,如果要把多个生成器连起来用,那么笔者强烈建议优先考虑 yield from 表达式。