0
点赞
收藏
分享

微信扫一扫

Python实现协程(三)

Brose 2021-09-28 阅读 55

一. 让协程返回值

下面的例子,我们再次改版之前计算平均值的协程函数,这一版本的协程函数每次被激活时,不会自动产出平均值,而是在最后返回一个值。

from collections import namedtuple
 
Result = namedtuple('Result', 'count average')
 
 
def average():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total / count
    return Result(count, average)

为了返回值,协程必须正常终止,因此 average 中有个判断条件,以便退出累计循环。返回值为 namedtuple,包含 countaverage 两个字段。

演示 1 发送 None 会终止循环,导致协程结束

注意:协程结束,协程对象会抛出 StopIteration 异常。return 表达式的值通过异常对象 StopIteration 传递给调用方。这样做有点不合常理,但是能保留生成器对象的常规行为,即在耗尽时抛出 StopIteration 异常。

演示 2 获取协程 return 的值

捕获 StopIteration 异常,并通过异常对象的 value 属性获取 average 返回的值。

下面我们介绍的 yield from 结构会在内部自动捕获 StopIteration 异常,这种处理方式与 for 循环处理 StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异常。

yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value 属性的值变为 yield from 表达式的值。

二. 使用 yield from

首先要明确,yield from 是全新的语法结构,其功能要比 yield 丰富。

演示 1 yield from 可用于简化 for 循环中的 yield 表达式

演示 2 使用 yield from 链接可迭代对象

yield from x 表达式对 x 对象所做的第一件事就是调用 iter(x) ,从中获取迭代器。因此 x 可以是任何可迭代的对象。

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。

在举例前,我们先来了解几个专门的术语:

  • 委派生成器:包含 yield from <iterable> 表达式的生成器函数。
  • 子生成器:从 yield from 表达式中 <iterable> 部分获取的生成器。
  • 调用方:调用委派生成器的客户端代码。在适当的语境中,将使用“客户端”代指“调用方”,以便于委派生成器(也是调用方,因为它调用了子生成器)区分开。

委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回的值附加到异常对象上,此时委派生成器会恢复。

示例 使用 yield from 计算平均值,并输出统计报告

from collections import namedtuple
 
Result = namedtuple('Result', 'count, average')
 
 
# 子生成器
def average():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average)
 
 
# 委派生成器
def grouper(results, key):
    while True:
        results[key] = yield from average()
        print(results[key])
 
 
# 客户端代码,即调用方
def main(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key)
        next(group)
        for value in values:
            group.send(value)
        group.send(None)
    report(results)
 
 
# 输出报告
def report(results):
    print('-'*20, 'report', '-'*20)
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print(f'{result.count:2} {group:5} averaging {result.average:.2f}{unit}')
 
 
data = {
    'girls;kg': [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    'girls;m': [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg': [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m': [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], }
 
if __name__ == '__main__':
    main(data)

运行结果:外层 for 循环每次迭代会产生一个 grouper 实例,赋值给 group 变量,group 是委派生成器。

调用 next(group),预激委派生成器 group,此时进入 while 循环,调用子生成器 average 后,在 yield from 表达式处暂停。内层 for 循环调用 group.send(value),直接把值传给子生成器 average。同时,当前的 groupyield from 表达式处暂停。

内层循环结束后,group 实例依旧在 yield from 表达式处暂停。因此,grouper 函数定义体中为 results[key] 赋值的语句还没有执行。如果外层 for 循环的末尾没有 group.send(None) ,那么 average 子生成器永远不会终止,委派生成器 group 永远不会再次激活,因此永远不会为 results[key] 赋值。

外层 for 循环重新迭代时会新建一个 grouper 实例,然后绑定到 group 变量上。前一个 grouper 实例,以及它创建的尚未终止的 average子生成器实例,被垃圾回收机制回收。

注意:

  • group.send(None) 以及 average 循环中的条件判断是至关重要的终止条件。如果不这么做,使用 yield from 调用这个协程的生成器会永远阻塞。
  • 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。
  • grouper 是委派生成器;
  • 这个循环每次迭代时会新建一个 average 实例,每个实例都作为协程使用的生成器对象。
  • grouper 发送的每个值都会经由 yield from 处理,通过管道传给 average 实例。group 会在 yield from 表达式处暂停,等待 average 实例处理客户端发来的值。average 实例运行完毕后,返回的值绑定到 results[key] 上。while 循环会不断创建 average 实例,处理更多的值。
  • 把各个 value 传给 grouper ,传入的值最终到达 averager 函数中 term = yield 那一行;grouper 永远不知道传入的值是什么。
  • None 传入 grouper ,导致当前的 averager 实例终止,也让 grouper 继续运行,再创建一个 averager 实例,处理下一组值。

这个实验想表名的关键一点是,如果子生成器不终止,委派生成器会在 yield from 表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from 把控制权交给客户代码(即委派生成器的调用方)了。

yield from 的意义

  • 子生成器产出的值都直接传给委派生成器的调用方。
  • 使用 send 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None ,那么会调用子生成器的 __next__ 方法。如果发送的值不是 None ,那么会调用子生成器的 send 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其它异常都会向上冒泡,传给委派生成器。
  • 生成器退出时,生成器中的 return expr 表达式会触发 StopIteration 异常抛出。
  • yield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数。

关于 yield 实现生成器的介绍至此已经完成,下一节我们将使用 asyncio 来实现协程。

举报

相关推荐

0 条评论