Python 中的协程
概述
Python 3.5 之前的协程是靠 yield 实现的,和生成器 yield 共用关键字,语义不明确,使用比较晦涩,很少有人使用(起码大多数爬虫程序用的是多线程)。Python 3.5 增加了 async 和 await 关键字(保留关键字,未正式确定,Python 3.7 正式确定),作为定义协程的专用关键字。协程才正式变得优雅可用,不过它的基础仍是基于 yield 的协程。作为基础,我们对其做一下简述。
与生成器的不同
协程、线程、进程的区别不在赘述。简述协程和生成器的区别:
- 生成器是用于生成供迭代的数据
- 协程是数据的消费者
- 虽然在协程中会使用
yield产生值,但这与迭代无关。
也就是说,协程只是和生成器 “碰巧” 共用了 yield 关键词,其他无任何关联。
协程基础
下面我们来分析下《流畅的 Python》中协程的一个例子:
简单实例
1 | def simple_coroutine(): # ① |
- ① 协程使用生成器函数定义:定义体重有
yield关键字。 - ④ 首选要调用
next(...)函数,因为生成器还没有启动,没在yield语句处暂停,所以一开始发送数据。也可以用给生成器发送None代替:my_coro,send(None),专业术语叫预激生成器。
可用 inspect.getgeneratorstate(...) 查看协程状态:
1 | import inspect |
具体有协程从创建到结束有四种状态:
GEN_CREATED: 等待执行GEN_RUNNING: 解析器正在执行GEN_SUSPENDED: 在yield表达式出暂停执行GEN_CLOSED: 执行结束
只有在多线程应用中才能看到 GEN_RUNNING 状态。此外,生成器对象在自己身上调用 getgeneratorstate 函数能看到,可自行测试。
预激协程
为了方便预激协程,《流畅的 Python》提供了一个预激协程的装饰器例子,可供参考:
1 | from functools import wraps |
终止协程和异常处理
在协程中,未处理的异常能导致协程终止
Python 也为协程提供两个方法:
generator.throw(exc_type[, exc_value[, traceback]]):致使生成器在暂停yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产生的值会调用generator.throw方法得到返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。generator.close():只是生成器在暂停的yield表达式处抛出GeneratorExit异常。
让协程返回值
return 可以出现在生成器中,当生成器正常结束,执行 return。
1 | def test(): |
1 | t = test() |
1 | t = test() |
实例分析
1 | def simple_coro(a): |
调用:
1 | coro = simple_coro(14) # 创建协程 |
从上述分析中我们看到,yield 的作用,不严谨的说是:
- 协程在
yield关键字所在位置暂停执行; - 当协程执行至此
yield处时,返回yield右边表达式的值,并等待send函数发送下一个值给yield左边的变量; - 每一次
coroutine.send(value)执行的范围为:yield语句行左边变量赋值至下一个yield语句行右边表达式返回值或协程结束。
如上述协程 coro.send(28) 的执行范围为:
1 | _________ |
yield from
基础概念:
- 委派生成器:包含
yield from <iterable>表达式的生成器函数。 - 子生成器:从
yield from表达式中<iterable>部分获取的生成器(subgenerator)。 - 调用方:调用委派生成器的客户端代码。