上一篇文章中,我们介绍了 Python 中的 yield 关键字以及依赖其实现的生成器函数。 python 中的迭代器与生成器
生成器函数在形式上与协程已经十分接近,本文我们就来详细介绍一下协程。
协程又称为微线程,虽然整个执行过程中只有一个线程,但某个方法的执行过程中可以挂起、让出CPU给另一个方法,等到适当的时机再回到原方法继续执行,但两个方法之间并没有相互调用关系,他们类似于系统中断或多线程的表现。 由此,我们可以看到协程具有以下优势:
>>> def simple_coroutine():
... print('-> coroutine started')
... x = yield
... print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
...
StopIteration可以看到,在上面的例子中,yield 不再是我们所熟悉的出现在式子的左边,而是成为了变量赋值的右值,事实上,此处 yield 右侧同样可以出现值、变量或表达式。 当程序执行到 yield 表达式时,协程被挂起,同时返回 yield 右侧的值(如果有的话) 对这个协程执行 send 操作实际上就是将 send 方法的参数传递给 yield 表达式的左值,接着程序继续运行下去。
协程有以下四种状态:
通过使用 inspect.getgeneratorstate 函数可以返回上述四个中的一个状态字符串。 只有当一个协程处于 GEN_SUSPENDED 状态时才可以调用其 send 方法,否则会抛出异常:
>>> 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>>> 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'下图展示了上述代码的执行过程:

因此需要首先调用 next 方法,让协程执行到第一个 yield 表达式,这一过程被称为“预激”(prime) 所有协程都必须预激然后使用,这一次 next 调用看上去总是让人觉得有些多余,而没有他又会报错,所以我们可以在自己的协程上加一个装饰器,以使协程被创建后自动完成预激功能。
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return primer关于装饰器的内容,可以参考: python 中的装饰器及其原理
有下面几种情况会让协程进入 GEN_CLOSED 状态:
关于 Python 的垃圾回收机制,参考: python 的内存管理与垃圾收集
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可以看到,上例中,协程是一个无限循环,只要调用方不断将值发送给协程,他就会不断累加、计算移动平均数,直到协程的 close 方法被调用或协程对象被垃圾回收。
下面的例子中,我们在上面计算移动平均数的代码最后加上了返回语句。
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(30)
>>> coro_avg.send(6.5)
>>> coro_avg.send(None)
Traceback (most recent call last):
...
StopIteration: Result(count=3, average=15.5)可以看到,在最终给协程发送 None 导致协程退出后,抛出的 StopIteration 中携带了这个返回值,通过 StopIteration 的 value 字段我们可以取出该值:
yield from 语句可以简化生成器函数中的 yield 表达式,这在我们此前的文章中已经介绍过:
>>> def gen():
... for c in 'AB':
... yield c
... for i in range(1, 3):
... yield i
...
>>> list(gen())
['A', 'B', 1, 2]可以改写成:
>>> def gen():
... yield from 'AB'
... yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]包含 yield from 语句的函数被称为委派生成器,他打开了双向通道,将最外层的调用方与最内层的子生成器连接起来,让二者可以直接发送和产出值,还可以直接传入异常,位于中间的协程无序添加任何中间处理的代码。 yield from 语句会一直等待子生成器终止并抛出 StopIteration 异常,而子生成器通过 return 语句返回的值会成为 yield from 语句的传入值。