把生成器当成协程

Python 2.2 引入了 yield 关键字实现的生成器函数,大约五年后,Python 2.5 实现了“PEP 342 — Coroutines via Enhanced Generators”https://www.python.org/dev/peps/pep-0342/ 这个提案为生成器对象添加了额外的方法和功能,其中最值得关注的是 .send() 方法。 与next() 方法一样,.send() 方法致使生成器前进到下一个 yield 语句。不 过,.send() 方法还允许使用生成器的客户把数据发给自己,即不管传给 .send() 方法 什么参数,那个参数都会成为生成器函数定义体中对应的 yield 表达式的值。也就是 说,.send() 方法允许在客户代码和生成器之间双向交换数据。而next() 方法只 允许客户从生成器中获取数据。 这是一项重要的“改进”,甚至改变了生成器的本性:像这样使用的话,生成器就变身为协程。

在 PyCon US 2009 期间举办的一场著名的课程中 http://www.dabeaz.com/coroutines/,David Beazley(可能是 Python 社区中在协程方面 最多产的作者和演讲者)提醒道:
.生成器用于生成供迭代的数据
.协程是数据的消费者
.不能把这两个概念混为一谈
.协程与迭代无关。虽然在协程中会使用yield产出值,但这与迭代无关。

生成器中的yield关键字

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

协程中的yield关键字

从句法上看,协程与生成器类似,都是定义体中包含yield关键字的函数。可是,在协程中,yield通常出现在表达式的右边(例如,datum = yield),可以产出值,也可以不产出——如果 yield 关键字后面没有表达式,那么生成器产出 None。协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是 next(…) 函数。通常,调用方会把值推送给协程。 yield 关键字甚至还可以不接收或传出数据。不管数据如何流动,yield都是一种流程控制工具,使用它可以实现协作式多任务:协程可以把控制器让步给中心调度程序,从而激活其他的协程。

从根本上把 yield 视作控制流程的方式,这样就好理解协程了。

生成器如何进化成协程

协程的底层架构在“PEP 342—Coroutines via Enhanced Generators”https://www.python.org/dev/peps/pep-0342/中定义,并在 Python 2.5(2006 年)实现了。自此之后,yield 关键字可以在表达式中使用,而且生成器 API 中增加了 .send(value) 方法。生成器的调用方可以使用 .send(…) 方法发送数据,发送的数据 会成为生成器函数中 yield 表达式的值。因此,生成器可以作为协程使用。协程是指一 个过程,这个过程与调用方协作,产出由调用方提供的值。

除了 .send(…) 方法,PEP 342还添加了.throw(…)和.close()方法:.throw(…)的作用是让调用方抛出异常,在生成器中处理;.close()的作用是终止生成器。

用作协程的生成器的基本行为

示例 可能是协程最简单的使用演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>> def simple_coroutine(): # 协程使用生成器函数定义:定义体中有 yield 关键字。
  ...     print('-> coroutine started')
  ...     # yield 在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是 None ——这个值是隐式指定的,
  ...     # 因为 yield 关键字右边没有表达式。
  ...     x = yield  
  ...     print('-> coroutine received:', x)
  ...
>>> my_coro = simple_coroutine()
>>> my_coro  # 与创建生成器的方式一样,调用函数得到生成器对象。
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro)  # 首先要调用 next(...) 函数,因为生成器还没启动,没在 yield 语句处暂停,所以一 开始无法发送数据。
-> coroutine started
>>> my_coro.send(42) # 调用这个方法后,yield表达式会计算出42;现在,协程会恢复,一直运行到下一个yield表达式,或者终止。
-> coroutine received: 42
Traceback (most recent call last):  # 这里,控制权流动到协程定义体的末尾,导致生成器像往常一样抛出 StopIteration 异常。
...
StopIteration

协程可以身处四个状态中的一个。当前状态可以使用 inspect.getgeneratorstate(…) 函数确定,该函数会返回下述字符串中的一个。 ‘GEN_CREATED’ 等待开始执行。

‘GEN_RUNNING’ 解释器正在执行。

‘GEN_SUSPENDED’ 在 yield 表达式处暂停。

‘GEN_CLOSED’ 执行结束。

因为 send 方法的参数会成为暂停的 yield 表达式的值,所以,仅当协程处于暂停状态时 才能调用 send 方法,例如 my_coro.send(42)。不过,如果协程还没激活(即,状态是 ‘GEN_CREATED’),情况就不同了。因此,始终要调用 next(my_coro) 激活协程——也 可以调用 my_coro.send(None),效果一样。 如果创建协程对象后立即把 None 之外的值发给它,会出现下述错误:

1
2
3
4
5
>>> my_coro = simple_coroutine()
>>> my_coro.send(1729)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator

最先调用 next(my_coro) 函数这一步通常称为“预激”(prime)协程(即,让协程向前执 行到第一个 yield 表达式,准备好作为活跃的协程使用)。

下面举个产出多个值的例子,以便更好地理解协程的行为。

示例 产出两个值的协程

 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
>>> def simple_coro2(a):
...     print('-> Started: a =', a)
...     b = yield a
...     print('-> Received: b =', b)
...     c = yield a + b
...     print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2) # ❶
'GEN_CREATED'
>>> next(my_coro2) # ❷
-> Started: a = 14 14
>>> getgeneratorstate(my_coro2) # ❸
'GEN_SUSPENDED'
>>> my_coro2.send(28) # ❹
-> Received: b = 28
42
>>> my_coro2.send(99) # ➎
-> Received: c = 99
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2) # ➏
'GEN_CLOSED'

❶ inspect.getgeneratorstate 函数指明,处于 GEN_CREATED 状态(即协程未启 动)。
❷ 向前执行协程到第一个 yield 表达式,打印 -> Started: a = 14 消息,然后产出 a 的值,并且暂停,等待为 b 赋值。
❸ getgeneratorstate 函数指明,处于 GEN_SUSPENDED 状态(即协程在 yield 表达式 处暂停)。
❹ 把数字 28 发给暂停的协程;计算 yield 表达式,得到 28,然后把那个数绑定给 b。 打印 -> Received: b = 28 消息,产出 a + b 的值(42),然后协程暂停,等待为 c 赋值。
❺ 把数字 99 发给暂停的协程;计算 yield 表达式,得到 99,然后把那个数绑定给 c。 打印 -> Received: c = 99 消息,然后协程终止,导致生成器对象抛出 StopIteration 异常。
❻ getgeneratorstate 函数指明,处于 GEN_CLOSED 状态(即协程执行结束)。

关键的一点是,协程在 yield 关键字所在的位置暂停执行。前面说过,在赋值语句中,= 右边的代码在赋值之前执行。因此,对于 b = yield a 这行代码来说,等到客户端代码 再激活协程时才会设定 b 的值。

simple_coro2 协程的执行过程分为 3 个阶段,如图 所示
(1) 调用 next(my_coro2),打印第一个消息,然后执行 yield a,产出数字 14。
(2) 调用 my_coro2.send(28),把 28 赋值给 b,打印第二个消息,然后执行 yield a + b,产出数字 42。
(3) 调用 my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协程终止。

Python-Coroutine.PNG

执行simple_coro2 协程的3个阶段(注意,各个阶段都在yield表达式中结束,而且下一个阶段都从那一行代码开始,然后再把yield表达式的值赋给变量)


转载请注明本网址。