本章涵盖以下话题:
- 生成器作为协程使用时的行为和状态
- 使用装饰器自动预激协程
- 调用方如何使用生成器对象的.close()和.throw()方法控制协程
- 协程终止时如何返回值
- yield from新句法的用途和语义
16.1 生成器如何进化成协程
协程是指一个过程,这个过程与调用方协作,产生由调用方提供的值。
16.2 用作协程的生成器的基本行为
示例16-1 可能是协程最简单的使用演示
>>> def simple_coroutine():
... print('-> coroutine started')
... x = yield
... print('-> coroutine received:', x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x7f7fc81e9db0>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
协程可以身处四个状态中的一个。当前状态可以使用inspect.getgeneratorstate(...)函数确定,该函数会返回下述字符串中的一个。
‘GEN_CREATE’
等待开始执行
‘GEN_RUNNING’
解释器正在执行
‘GEN_SUSPENDED’
在yield表达式处暂停
‘GEN_CLOSED’
执行结束
始终要先调用next(my_coro)激活协程————也可以调用my_coro.send(None)。否则会出现下述错误
>>> 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
>>>
示例16-2 产出两个值协程
>>> def simple_coro2(a):
... print('-> Started: a=', a)
... b = yield a
... print('-> Received: b=', a)
... 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= 14
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'
>>>
执行simple_coro2协程的3个阶段
16.3 示例:使用协程计算移动平均值
示例16-3 定义一个计算移动平均值的协程
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
10.0
coro_avg.send(30)
20.0
coro_avg.send(5)
15.0
16.4 预激协程的装饰器
如果不预激,那么协程没什么用。调用my_coro.send(x)之前,记住一定要调用next(my_coro)。为了简化协程的用法,有时会有一个预激装饰器。
示例16-5 预激协程的装饰器
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return primer
示例16-6
用于计算移动平均值的协程
coro_avg = averager()
from inspect import getgeneratorstate
getgeneratorstate(coro_avg)
'GEN_SUSPENDED'
coro_avg.send(10)
10.0
coro_avg.send(30)
20.0
coro_avg.send(5)
15.0
from coroutil import coroutine
@coroutine
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
16.5 终止协程和异常处理
协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方
示例16-7
coro_avg = averager()
coro_avg.send(40)
40.0
coro_avg.send(50)
45.0
coro_avg.send('spam')
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
total += term
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
getgeneratorstate(coro_avg)
'GEN_CLOSED'
coro_avg.send(60) #由于在协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIterator
Traceback (most recent call last):
File "<input>", line 1, in <module>
StopIteration
从Python2.5开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。这个两个方法是throw和close。
generator.throw(exc_type[,exc_value[, traceback]])
致使生成器在暂停的yield表达式外抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产出的值会成为调用generator.throw方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。
generator.close()
致使生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有出来这个异常,或者抛出了StopIterator异常(通常是指运行到结尾),调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。生成器抛出的其他异常会向上冒泡,传给调用方。
示例16-8 coro_exc_demo.py:学习在协程中处理异常的测试代码
class DemoException(Exception):
pass
def demo_exc_handling():
print('-> coroutine started')
while True:
try:
x = yield
except DemoException:
print('*** DemoException handled. Continuing...')
else:
print('-> coroutine received: {!r}'.format(x))
raise RuntimeError('This line should never run.')
示例16-9 激活和关闭demo_exc_handling,没有异常
exc_coro = demo_exc_handling()
next(exc_coro)
-> coroutine started
exc_coro.send(11)
-> coroutine received: 11
exc_coro.send(22)
-> coroutine received: 22
exc_coro.close()
from inspect import getgeneratorstate
getgeneratorstate(exc_coro)
'GEN_CLOSED'
示例16-10 把DemoException异常传入demo_exc_handling不会导致协程中止
exc_coro = demo_exc_handling()
next(exc_coro)
-> coroutine started
exc_coro.send(11)
-> coroutine received: 11
exc_coro.throw(DemoException)
*** DemoException handled. Continuing...
getgeneratorstate(exc_coro)
'GEN_SUSPENDED'
示例16-11 如果无法处理传入的异常,协程会中止
exc_coro = demo_exc_handling()
next(exc_coro)
-> coroutine started
exc_coro.send(11)
-> coroutine received: 11
exc_coro.throw(ZeroDivisionError)
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
x = yield
ZeroDivisionError
如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入try/finally块中
示例16-12 coro_finally_demo.py:使用try/finally块在协程中止时执行操作
class DemoException(Exception):
pass
def demo_exc_handling():
print('-> coroutine started')
try:
while True:
try:
x = yield
except DemoException:
print('*** DemoException handled. Continuing...')
else:
print('-> coroutine received: {!r}'.format(x))
finally:
print('-> coroutine ending')
16.6 让协程返回值
示例16-13 coroaverager2.py:定义一个求平均值的协程,让它返回一个结果
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):
File "<input>", line 1, in <module>
StopIteration: Result(count=3, average=15.5)
注意,return表达式的值会偷偷传给调用方,赋值给StopIteration异常的一个属性。
示例16-15 捕获StopIteration异常,获取averager返回的值
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(6.5)
try:
coro_avg.send(None)
except StopIteration as exc:
result = exc.value
result
Result(count=3, average=15.5)
16.7 使用yield from
yield from可用于简化for循环中的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]
示例16-16 使用yield from链接可迭代对象
>>> def chain(*iterables):
... for it in iterables:
... yield from it
...
>>> s = 'ABC'
>>> t = tuple(range(3))
>>> list(chain(s,t))
['A', 'B', 'C', 0, 1, 2]
yield from x表达式对x对象所做的第一件事是,调用iter(x),从中获取迭代器。
yield from结构的本质作用无法通过简单的可迭代对象说明,而要发散思维,使用嵌套的生成器。
yield from的主要作用是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。
委派生成器
包含yield from 表达式的生成器函数
子生成器
从yield from表达式中部分获取的生成器。
调用方
指代调用委派生成器的客户端代码。
调用方 --> 委派生成器-->子生成器
委派生成器在yield from表达式处暂停时,调用方可以直接把数据发送给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,此时委派生成器会恢复