编写高质量Python (第33条) 通过 yield from 把多个生成器连起来用

第 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 表达式。

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值