协程是个子程序,执行过程中,内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行
从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数。可是,在协
程中,yield 通常出现在表达式的右边(例如,datum = yield),可以产出值,也可
以不产出——如果 yield 关键字后面没有表达式,那么生成器产出 None。协程可能会从
调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是
next(...) 函数。通常,调用方会把值推送给协程。
yield 关键字甚至还可以不接收或传出数据。不管数据如何流动,yield 都是一种流程控
制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激
活其他的协程。
从根本上把 yield 视作控制流程的方式,这样就好理解协程了。
使用协程要注意的地方是
协程可以身处四个状态中的一个。当前状态可以使用
inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个。
'GEN_CREATED'
等待开始执行。
'GEN_RUNNING'
解释器正在执行。
'GEN_SUSPENDED'
在 yield 表达式处暂停。
'GEN_CLOSED'
执行结束。
协程创建之后,需要激活,需要调用next函数来激活协程。
为了避免忘记,我们可以在协程上使用一个特殊的装饰器来帮助激活协程
from functools import wraps def coroutine(func): @wraps(func) def primer(*args, **kwargs): gen = func(*args, **kwargs) next(gen) return gen return primer
协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触发协程的
对象)
终止协程的一种方式:发送某个哨符值,让协程退出。内置的 None 和
Ellipsis 等常量经常用作哨符值。Ellipsis 的优点是,数据流中不太常有这个值。我
还见过有人把 StopIteration 类(类本身,而不是实例,也不抛出)作为哨符值;也就
是说,是像这样使用的:my_coro.send(StopIteration)。
从 Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协
程。
这两个方法是 throw 和 close。
generator.throw(exc_type[, exc_value[, traceback]])
致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异
常,代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw
方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上
下文中。
generator.close()
致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。如果生成器没有处
理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报
错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出
RuntimeError 异常。生成器抛出的其他异常会向上冒泡,传给调用方。
协程在遇到异常或者哨符值是会退出。
协程在耗尽时会抛出StopIteration异常
对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把
value 属性的值变成 yield from 表达式的值。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起
来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中
添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职
责。
委派生成器
包含 yield from <iterable> 表达式的生成器函数。
子生成器
从 yield from 表达式中 <iterable> 部分获取的生成器。
调用方
PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境
中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调用了子
生成器)区分开。
最后给个实例
from collections import namedtuple Result = namedtuple('Result', 'count average') def averager(): total = 0.0 count = 0 average = None while True:
#此处接受send发送的值,发送None则退出协程,协程处于上下文管理器中,所以数据会叠加,只有第一次激活协程的使用才会初始化total=0,count=0,average=None 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 averager() 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) # print(results) report(results) def report(results): for key, result in sorted(results.items()): group, unit = key.split(';') print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, 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)
总结一下,使用协程的时,记得要激活协程,并且在协程耗尽时捕获异常,当然可以使用yield from来自动捕获,将协程看成是流程控制可能会好理解的多。
大部分内容摘自流畅的python第十六章