第十六章 - 协程
协程
1、生成器用于生成供迭代的数据,协程是数据的消费者
2、to yield: 产出,让步
3、yield流程控制工具,使用它可以实现协作式多任务;协程可以把控制器让步给中心调度程序,从而激活其他的协程。
4、yield from实现协作式多线程
5、在Python3.5中 async替代asyncio.coroutine, await代替 yield from
6、async def定义协程,async for 定义异步迭代器
7、直接使用 .next .send方法比较low, 应该使用 asyncio模块来实现异步协程
8、
16.1 生成器如何进化成协程
生成器的调用方可以使用.send(...)方法发送数据,发送的数据会成为生成器函数中yield表达式的值,因此生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值(生产者消费者模型)。
注意:在Python3.3以前, 如果在生成器中给return语句提供值,会抛出SyntaxError异常。
16.2 用作协程的生成器的基本行为
协程有四个状态:
1、GEN_CREATED
2、GEN_RUNNING
3、GEN_SUSPENDED
4、GEN_CLOSED
因为send方法的参数会成为暂停的yield表达式的值,所以仅当协程处于暂停状态时才能调用send方法。
如果协程在GEN_CREATED状态,发送None之外的值给它,会出现以下报错:
def simple_coroutine(): print("-> coroutine started") x = yield print("-> coroutine received:", x) my_coro = simple_coroutine() from inspect import getgeneratorstate print(getgeneratorstate(my_coro)) next(my_coro) print(getgeneratorstate(my_coro)) try: my_coro.send(23) except StopIteration: pass print(getgeneratorstate(my_coro))
>>>>
GEN_CREATED
-> coroutine started
GEN_SUSPENDED
-> coroutine received: 23
GEN_CLOSED
因此next(my_coro)函数这一步通常称为“预激”协程。
16.3 示例:使用协程计算移动平均值
def averager(): total = 0.0 count = 0 average = None while True: term = yield average total += term count += 1 average = total/count coro_avg = averager() next(coro_avg) # 预激活协程 print(coro_avg.send(10)) print(coro_avg.send(30)) print(coro_avg.send(50)) >>>> 10.0 20.0 30.0
16.4 预激活协程的装饰器
from functools import wraps def coroutine(func): """预激活协程装饰器,自动执行到第一个yield处使协程处于SUSPENDED状态""" @wraps(func) def primer(*args, **kwargs): gen = func(*args, **kwargs) next(gen) return gen return primer
使用@coroutine装饰器
from coroutil import coroutine @coroutine def averager(): total = 0.0 count = 0 average = None while True: term = yield average total += term count += 1 average = total/count coro_avg = averager() print(coro_avg.send(10)) print(coro_avg.send(30)) print(coro_avg.send(50)) >>> 10.0 20.0 30.0
16.5 终止协程和异常处理
协程中未处理的异常会向上冒泡,传给next函数或send方法的调用发(即触发协程的对象)。
从Python2.5开始,客户代码可以在生成器对象上调用两个方法,显示地把异常发给协程:
generator.throw(exc_type[, exc_value[, traceback]])
致使生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式。而产出的值会成为调用generator.throw方法得到的返回值。
generator.close()
致使生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出了StopIteration异常,调用方不会报错。
示例: generator.throw()
from inspect import getgeneratorstate class DemoException(Exception): """测试""" def demo_exc_handling(): print("-> coroutine started") while True: try: x = yield except DemoException: print("*** DemoExcption handled. Continuing...") else: print("-> coroutine received: {!r}".format(x)) raise RuntimeError("This line should never run.") exc_coro = demo_exc_handling() next(exc_coro) exc_coro.send(11) exc_coro.send(22) exc_coro.throw(DemoException) print(getgeneratorstate(exc_coro)) >>>> -> coroutine started -> coroutine received: 11 -> coroutine received: 22 *** DemoExcption handled. Continuing... GEN_SUSPENDED
示例: generator.close()
from inspect import getgeneratorstate class DemoException(Exception): """测试""" def demo_exc_handling(): print("-> coroutine started") while True: try: x = yield except DemoException: print("*** DemoExcption handled. Continuing...") else: print("-> coroutine received: {!r}".format(x)) raise RuntimeError("This line should never run.") exc_coro = demo_exc_handling() next(exc_coro) exc_coro.send(11) exc_coro.send(22) exc_coro.close() print(getgeneratorstate(exc_coro)) >>> -> coroutine started -> coroutine received: 11 -> coroutine received: 22 GEN_CLOSED
16.6 让协程返回值
示例1: 无异常处理
from collections import namedtuple Result = namedtuple("Result", "count, average") def averager(): 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) # python3.3之前,如果生成器返回值,解释器会报语法错误 coro_avg = averager() next(coro_avg) coro_avg.send(10) coro_avg.send(20) coro_avg.send(30) coro_avg.send(None) >>>> StopIteration: Result(count=3, average=20.0)
示例2: StopIteration异常捕获
from collections import namedtuple Result = namedtuple("Result", "count, average") def averager(): 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) # python3.3之前,如果生成器返回值,解释器会报语法错误 coro_avg = averager() next(coro_avg) coro_avg.send(10) coro_avg.send(20) coro_avg.send(30) try: coro_avg.send(None) except StopIteration as exc: result = exc.value print(result) >>>> Result(count=3, average=20.0) # 捕获StopIteration的到的结果
注意: yield from结构会在内部自动捕获StopIteration异常,这种处理方式与for循环处理StopIteration异常的方式一样。对于yield from结果来说, 解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from表达式的值。
16.7 使用yield from
在生成器中使用yield from subgen()时, subgen会获得控制权, 把产出的值传给gen的调用方,即调用方可以直接控制subgen, 与此同时, gen会阻塞,等待subgen终止。
def gen(): yield from "ABC" yield from range(0, 10) print(list(gen()))
yield from x 表达式对x 对象所做的第一件事是, 调用iter(x), 从中获取迭代器。
yield from的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者就可以直接发送和产出值。
PEP380中的术语:
委派生成器
包含yield from <iterable> 表达式的生成器函数
子生成器
从yield from表达式中<iterable>部分获取的生成器
调用方
调用委派生成器的客户端代码
示例:
from collections import namedtuple from coroutil import coroutine Result = namedtuple("Result", "count average") # 子生成器 def averager(): 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) # 委派生成器 @coroutine def grouper(results, key): while True: results[key] = yield from averager() # 一直等待averager()抛出StopIteration异常,然后再恢复运行(将StopIteration异常的第一个参数赋值给 results[key]),否则并且相当于一个中转站 将调用方的send来的数据,直接中转给averager。 # 客户端代码,调用方 def main(data): results = {} for key, values in data.items(): group = grouper(results, key) for value in values: group.send(value) group.send(None) print(results) 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) >>>> {'girls;m': Result(count=10, average=1.4279999999999997), 'girls;kg': Result(count=10, average=42.040000000000006), 'boys;m': Result(count=9, average=1.3888888888888888), 'boys;kg': Result(count=9, average=40.422222222222224)}
任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(...)函数或.sen(...)方法,可以隐式调用,例如使用for循环。
16.8 yield from的意义
“把迭代器当作生成器使用,相当于把子生成器的定义体内联在yield from表达式中。此外,子生成器可以执行return语句,返回一直值,而返回的值会成为yield from表达式的值。”
1、子生成器才产出的值都直接传给委派生成器的 --- 调用方
2、使用send()方法给委派生成器的值都直接传递给 子生成器。如果发送的值是None,那么会调用子生成器的__next__方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行。
3、生成器退出时,生成器中的return expr表达式会触发StopIteration。
4、yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
5、传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法,如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。
6、如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么会在子生成器上调用close()方法。
本章小结:
本章对协程的定义是宽泛的、不正式的,即:通过 调用方通过.send(...)方法发送数据或使用yield from结构驱动的生成器函数。
在asyncio库构建的协程采用更严格的方式:
1、使用@asyncio.coroutine装饰器装饰
2、始终使用yield from结构驱动,而不是通过直接在协程上调用.send(...)方法驱动