python实现协程(三)

一. 让协程返回值

        下面的例子,我们再次改版之前计算平均值的协程函数,这一版本的协程函数每次被激活时,不会自动产出平均值,而是在最后返回一个值。(averager协程返回的结果是一个namedtuple,2个字段分别是count和average):

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,包含count和average两个字段。(注:python3.3之前如果生成器返回值,解释器会报句法错误)。 

演示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丰富。在python 3.5以后的版本中使用async、await关键字取代yield from,虽然语法上略有差异,但是原理是一样的。理解了yield from,那么async和await的使用自然水到渠成。

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

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

        yield from x表达式对x对象所做的第一件事就是调用iter(x),从中获取迭代器。因此x可以是任何可迭代的对象。但,如果yield from结果唯一的作用是替代产出值的for循环,这个结果可能永远不会被添加到python语言中。yield from结构的本质无法使用简单的可迭代对象说明,要发散思维,使用嵌套生成器。

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

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

        委派生成器在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。同时,当前的group在yield from表达式处暂停。内层循环结束后,group实例依旧在yield from表达式处暂停。因此,grouper函数定义体中为results[key]赋值的语句还没有执行。如果外层for循环的末尾没有group.send(None),那么average子生成器永远不会终止,委派生成器group永远不会再次激活,因此永远不会为results[key]赋值。外层for循环重新迭代时会新建一个grouper实例,然后绑定到group变量上。前一个grouper实例(以及它创建的尚未终止的average子生成器实例)被垃圾回收机制回收。

注意

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

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

2.2 yield from的意义

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

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

发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/105159468