Python 中的协程

如果 Python 书籍有一定的指导作用,那么(协程就是)文档最匮乏、最鲜为人知的 Python 特性,因此表面上看是最无用的特性。

字典为动词 “to yield” 给出了两个释义:产出和让步。对于 Python 生成器中的 yield 来说,这两个含义都成立。 yield item 这行代码会产出一个值,提供给 next(...) 的调用方;此外,还会做出让步,让 next(...) 调用方继续工作,直到需要使用另一个值时再调用 next()。调用方会从生成器中拉取值。

从句法上看,协程和生成器类似,都是定义体中包含 yield 关键字的函数。可是,在协程中,yield 通常出现在表达式的右边(例如:data = yield),可以产出值,也可以不产出——如果 yield 关键字后面没有表达式,那么生成器产出 None。协程可能从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(data) 方法,而不是 next(...) 函数。通常,调用方会把值推送给协程。

yield 关键字甚至可以不接受或者传出数据。不管数据怎么流动,yield 都是一种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激活其他的协程。

一个简单的协程

1
2
3
4
5
6
7
8
9
10
11
12
# simple_coroutine.py
def simple_coroutine():
print('-> coroutine started')
x = yield
print('-> coroutine received:', x)


my_coro = simple_coroutine()
print(my_coro) # 与创建生成器的方式一样,调用协程函数得到生成器对象
next(my_coro) # 预激协程
my_coro.send(47) # 向协程发送数据,协程中 x 接收到通过 send 函数发送的数据。
# 最后控制权流动到协程定义体的末尾,导致生成器像往常一样抛出 StopIteration 异常

输出:

1
2
3
4
5
6
<generator object simple_coroutine at 0x0000017DB0FE11A8>
-> coroutine started
-> coroutine received: 47
Traceback (most recent call last):
...
StopIteration

协程可以身处四个状态之一。当前状态可以使用 inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个:

状态 说明
GEN_CREATED 等待开始执行
GEN_RUNNING 解释器正在执行
GEN_SUSPENDED 在 yield 表达式处暂停
GEN_CLOSED 执行结束

因为 send 方法的参数会成为暂停的 yield 表达式的值,所以,仅当协程处于暂停状态时才能调用 send 方法。所以,在开始发送数据前,需要预激协程(一般是 send(None) 或者 next(my_coro)),让协程从 'GEN_CREATED' 状态转到 'GEN_SUSPENDED'

示例:使用协程计算移动平均值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# example_averager.py
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count


if __name__ == '__main__':
coro = averager()
next(coro) # 预激协程
print(coro.send(10))
print(coro.send(20))
print(coro.send(5))
coro.close()

averager() 函数中的无限循环表明,只要调用方不断把值发给这个协程,他就会一直接收值,然后生成结果。仅当调用方在协程上调用 .close() 方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。

通过装饰器预激协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# example_decorator.py
from functools import wraps

def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
print('do primer')
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


if __name__ == '__main__':
coro = averager()
from inspect import getgeneratorstate
print(getgeneratorstate(coro))
print(coro.send(10))
print(coro.send(20))
print(coro.send(5))

使用 yield from 句法调用协程时,会自动预激,因此与示例中的 @coroutine 等装饰器不兼容。Python 标准库里的 `@asyncio.coroutine装饰器不会预激协程,因此能兼容yield from` 句法。

终止协程和异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> from example_decorator import averager
>>> coro = averager()
do primer
>>> coro.send(40) # 直接发送数据,从 example_decorator 模块中导入的 averager() 协程已经自动预激
40.0
>>> coro.send(59)
49.5
>>> coro.send('spam')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
>>> coro.send(12)
Traceback (most recent call last):
...
StopIteration

发送的值不是数字时协程内出现异常,协程会停止,并且异常冒泡到调用方。如果试图重新激活协程,会抛出 StopIteration 异常。

可以在协程的 yield 语句周围加上 try/except 块来捕获可能出现的异常,或者给异常附加信息,然后向上冒泡。

限于篇幅,异常处理不多讲,在 asyncio 中会详细讲异常处理。

不论何时,只要出现了异常,协程的状态都会变成 'GEN_CLOSED',所以不能再重新激活协程。

让协程返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)


if __name__ == '__main__':
coro = averager()
next(coro) # 预激协程
coro.send(10)
coro.send(30)
coro.send(5)
coro.send(None)

运行结果:

1
2
3
Traceback (most recent call last):
...
StopIteration: Result(count=3, average=15.0)

通过判断变量 term 是否为 None 来决定是否终止协程,终止协程时返回一个 Result 具名元组类对象,附加在 StopIteration 异常的信息中。

关于协程的介绍就到这里了,下一篇会讲 yield from 语句的定义与使用。

-------------本文结束感谢阅读-------------
  • 本文标题:Python 中的协程
  • 本文作者:xlui
  • 发布时间:2017年10月15日 - 21:10
  • 最后更新:2023年01月20日 - 01:01
  • 本文链接: https://xlui.me/t/python-coroutine/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!