协程
协程的状态
一个简单协程示例:
def simple_coroutine(): print('start') x = yield #yield右边为空,默认产出None print('received:', x) my_coro = simple_coroutine() print(my_coro) next(my_coro) #启动生成器 my_coro.send(40) #调用后,yield表达式会计算出40,然后执行到下一个yield或终止
#结果 <generator object simple_coroutine at 0x000001C17BB43A98> #生成器对象 start received: 40 StopIteration #与生成器行为一样,抛出StopIteration异常
类似于线程、进程,协程可以身处四个状态中的某一个:
GEN_CREATED //就绪,等待开始执行
GEN_RUNN //执行,解释器正在执行
GEN_SUSPENDED //暂停,在yield表达式处暂停
GEN_CLOSED //结束,执行结束
协程状态在python中可以使用inspect.getgeneratorstate()函数获取:
def simple_coroutine_2(a): print('start:a = ', a) b = yield a print('received:b = ', b) c = yield a + b print('received:c = ', c) my_coro = simple_coroutine_2(14) from inspect import getgeneratorstate print(getgeneratorstate(my_coro)) next(my_coro) #执行到第一个yield,产出a的值,等待为b赋值 print(getgeneratorstate(my_coro)) my_coro.send(28) #见结果,b值为28,调用方将28发送给协程 print(getgeneratorstate(my_coro)) try: my_coro.send(99) except StopIteration: pass print(getgeneratorstate(my_coro)) #结果 GEN_CREATED #协程未启动 start:a = 14 GEN_SUSPENDED #协程暂停 received:b = 28 GEN_SUSPENDED #协程暂停 received:c = 99 GEN_CLOSED #协程结束
预激协程
在上一个示例中,未调用next()函数时,协程处于GEN_CREATED状态。处于GEN_CREATED状态的协程没有激活,须使用next()函数激活协程才能将值发送给协程,否则报错。
my_coro = simple_coroutine_2(14) my_coro.send(15) #结果 TypeError: can't send non-None value to a just-started generator
调用next()函数后,协程会向前执行到yield表达式,产出值,并暂停等待调用方发送值。
调用next()函数预激协程
或使用装饰器
from functools import wraps #定义一个预激协程的装饰器 def coroutine(func): @wraps(func) def primer(*args, **kwargs): gen = func(*args, **kwargs) #调用被装饰函数,获取生成器对象 next(gen) #预激协程 return gen #返回生成器 return primer #使用装饰器,计算历史平均值 @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() from inspect import getgeneratorstate print(getgeneratorstate(coro_avg)) print(coro_avg.send(10)) print(coro_avg.send(20)) #结果 GEN_SUSPENDED #一开始就处于暂停状态 10.0 15.0
控制协程
generator.send(value) //生成器调用方可以使用send发送数据,这个成为yield表达式的值,并前进到下一个yield处
generator.throw(exc_type[, exc_value[, traceback]]) //致使生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产出的值会调用generator.throw方法得到返回值。如果生成器没有处理抛出的异常,异常会向上冒泡。
generator.close() //致使生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出StopIteration异常,调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError。生成器抛出的其他异常会向上冒泡。
异常处理测试代码:
class DemoException(Exception): """此次演示的定义异常类型""" def demo_exc_handling(): print('start') while True: try: x = yield except DemoException: #特别处理 print('*** DemoException handled.Continuing...') else: print('received:{!r}'.format(x)) raise RuntimeError('this line should never run.') #这一行代码永远不会执行,未处理的异常才会终止循环,而一旦出现未处理的异常,协程会立刻终止
激活和关闭demo_exc_handling,没有异常:
exc_coro = demo_exc_handling() next(exc_coro) exc_coro.send(10) exc_coro.send(20) exc_coro.close() from inspect import getgeneratorstate print(getgeneratorstate(exc_coro)) #结果 start received:10 received:20 GEN_CLOSED
把DemoException异常传入demo_exc_handling协程,它会处理然后继续运行:
exc_coro = demo_exc_handling() next(exc_coro) exc_coro.send(10) exc_coro.throw(DemoException) from inspect import getgeneratorstate print(getgeneratorstate(exc_coro)) #结果 start received:10 *** DemoException handled.Continuing... GEN_SUSPENDED
尝试传入没有处理的异常:
exc_coro = demo_exc_handling() next(exc_coro) exc_coro.send(10) exc_coro.throw(ZeroDivisionError) from inspect import getgeneratorstate print(getgeneratorstate(exc_coro)) #结果 start received:10 #报错 GEN_CLOSED
若不管协程如何结束都想做一些清理工作,要把协程定义体中的相关代码放入try/finally块中:
class DemoException(Exception): """此次演示的定义异常类型""" def demo_exc_handling(): print('start') try: while True: try: x = yield except DemoException: print('*** DemoException handled.Continuing...') else: print('received:{!r}'.format(x)) finally: print('ending')
获取协程返回值
使用return直接返回值是可行的,但和普通函数不同,return语句的返回值会赋值给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) coro_avg = averager() next(coro_avg) coro_avg.send(10) coro_avg.send(20) coro_avg.send(None) #结果 StopIteration: Result(count=2, average=15.0)
捕获StopIteration异常,获取返回值:
coro_avg = averager() next(coro_avg) coro_avg.send(10) coro_avg.send(20) try: coro_avg.send(None) except StopIteration as e: print(e.value) #结果 Result(count=2, average=15.0)
yield from
它的一个用法是简化for循环中的yield表达式:
def gen(): for c in 'ABC': yield c for i in range(1, 5): yield i def gen_2(): yield from 'ABC' yield from range(1, 5)
两个函数是一样的功能。
在协程中yield from会创建通道,把内层生成器直接与外层生成器的客户端联系起来。二者可以直接发送和产出值,还可以直接传入异常。
一个至关重要的一点:在生成器gen中使用yield from subgen()时,subgen()会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。与此同时,gen会阻塞,等待subgen终止。
三个术语
委派生成器:包含yield from <iterable>表达式的生成器函数
子生成器:从yield from表达式中<iterable>部分获取的生成器。
调用方:调用生成器的客户端代码。
下面一个例子是:从data字典中读取虚构的七年级男女学生的体重和身高,获取生成平均结果。
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) #委派生成器 def grouper(result, key): while True: #每次迭代创建一个新的averager实例;每个实例都是作为协程使用的生成器 result[key] = yield from averager() #grouper发送的每个值都会经由yield from处理,通过管道传递给averager实例。grouper会在yield from #表达式暂停,等待averager处理客户端发来的值。 #调用方,即驱动函数 def main(data): results = {} for key, values in data.items(): group = grouper(results, key) #group是调用grouper函数得到的生成器对象,group作为协程使用 next(group) #预激协程 for value in values: #把各个value传给grouper,传入的值最终到达averager函数中term = yield这一个,grouper函数不知情 group.send(value) group.send(None) #终止当前averager实例 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.14, 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)
如果没有.send(None),那么averager子生成器不会终止,results[key]赋值语句不会执行。
例子表达最关键的一点是:如果子生成器不终止,委派生成器会在yield from表达式处永远暂停,这样程序不会向前执行,yield from把控制权转交给调用方了,但有任务没有执行。
委派生成器相当于管道,所以可以把任意数量的委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个生成器,以此类推。最终,这个链条要以一个yield表达式的简单生成器结束或以任何可迭代对象结束。任何yield from链条都必须由客户端驱动,在最外层委派生成器上调用next()函数或者.send()方法。
关于yield from 六点重要的说明:
- 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)
- 使用send()方法发送给委派生成器的值都直接传给子生成器。如果发送的值为None,那么会给委派调用子生成器的__next__()方法。如果发送的值不是None,那么会调用子生成器的send方法,如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行,任何其他异常都会向上冒泡,传给委派生成器
- 生成器退出时,生成器(或子生成器)中的return expr表达式会出发StopIteration(expr)异常抛出
- yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。yield from 结构的另外两个特性与异常和终止有关。
- 传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡,传给委派生成器
- 如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么在子生成器上调用clsoe()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器,否则委派生成器抛出GeneratorExit异常
以上来自《流畅的python》