Contents
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表达式的方案