yield与协程
Python中, 可以通过使用yield定义的生成器, 实现协程.
生成器
协程生成器的基本行为
首先定义一个生成器:
复制 def simple_coroutine():
print('-> coroutine started')
x = yield
print('-> coroutine received:', x)
形式如同定义函数, 如果定义体中含有yield关键字, 则就是一个生成器的定义体. 创建一个生成器对象:
复制 my_coro = simple_coroutine()
my_coro
输出为:
复制 <generator object simple_coroutine at 0x00000267ED4245C8>
协程有四种状态 :
GEN_SUSPENDED: 在yield表达式处暂停
可以使用inspect包中的inspect.getgeneratorstate
函数查看某个协程的状态:
复制 import inspect
inspect.getgeneratorstate(my_coro)
输出为:
一个刚创建的生成器, 其状态为GEN_CREATED , 此时的协程还没有被激活, 需要经过预激 (prime)操作, 让协程向前执行到第一个yield表达式 , 准备好作为活跃的协程使用. 预激的方法有两种:
使用协程的send方法, 并传递一个None, my_coro.send(None)
输出为:
可以看到, 此时的生成器执行到yield的表达式处, 并将控制权交还给了调用方 .
查看此时协程的状态:
复制 inspect.getgeneratorstate(my_coro)
输出为:
即状态变成了GEN_SUSPENDED, 代表在yield表达式处暂停.
继续调用:
输出为:
复制 -> coroutine received: 42
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-12-7c96f97a77cb> in <module>()
----> 1 my_coro.send(42)
StopIteration:
可以看出:
send方法输入的参数, 会被赋给yield表达式所在行的左侧的变量, 此处将42
赋给了参数x
生成器执行完之后, 抛出StopIteration
错误. 可以由调用方捕捉这个错误, 进行相应逻辑的处理
查看此时协程的状态:
复制 inspect.getgeneratorstate(my_coro)
输出为:
生成器, 也即协程已经正常结束了.
如果协程没有预激, 就直接使用send
方法传递参数, 会报如下的错位:
复制 my_coro1 = simple_coroutine()
print(inspect.getgeneratorstate(my_coro))
my_coro1.send(42)
输出为:
复制 GEN_CLOSED
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-14-c1f2b4df7ce8> in <module>()
1 my_coro1 = simple_coroutine()
2 print(inspect.getgeneratorstate(my_coro))
----> 3 my_coro1.send(42)
TypeError: can't send non-None value to a just-started generator
协程向调用方返回结果
生成器在执行到yield表达式暂停时, 会将yield表达式执行后的结果, 返回给调用方. 看下面的例子:
复制 def simple_coro2(a):
print('-> coroutine started: a =', a)
b = yield a # 返回a的值
print('-> Received: b =', b)
c = yield a + b # 返回a+b的值
print('-> Received: c =', c)
my_coro2 = simple_coro2(14) # 创建生成器
print(inspect.getgeneratorstate(my_coro2))
next(my_coro2) # 激活生成器
输出为:
复制 GEN_CREATED
-> coroutine started: a = 14
14
可以看到激活操作, 执行了第一个yield表达式, 将生成器中a
的值14
返回给了调用方, 并等待send
操作传入值. 继续调用send
方法, 会将send中指定的参数传递给b
, 向下执行, 将yield表达式a+b
的值返回:
输出为:
结果如同预期, 将a+b=42
的结果返回. 继续调用, 协程执行完毕.
输出为
复制 -> Received: c = 99
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-18-977093b924ab> in <module>()
----> 1 my_coro2.send(99)
StopIteration:
整个过程分三步:
调用next(my_coro2), 打印a
的值, 然后执行yield a, 产出数字14, 返回
调用my_coro2.send(28), 把28赋值给b, 打印b
的值, 然后执行 yield a + b, 产生数字42, 返回
调用my_coro2.send(99), 把99赋值给c, 打印c
的值, 产生数字99, 返回
使用装饰器预激协程
新创建的生成器对象, 每次都要手动进行预激, 比较麻烦, 可以用装饰器 进行预激操作:
复制 from functools import wraps
def coroutinue(func):
'''
装饰器: 向前执行到第一个`yield`表达式,预激`func`
:param func: func name
:return: primer
'''
@wraps(func)
def primer(*args, **kwargs):
# 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。
gen = func(*args, **kwargs)
# 调用被被装饰函数,获取生成器对象
next(gen) # 预激生成器
return gen # 返回生成器
return primer
定义生成器:
复制 @coroutinue
def simple_coro(a):
a = yield
直接创建的生成器对象就已经是激活的状态:
复制 coro = simple_coro(12)
inspect.getgeneratorstate(coro)
输出为:
终止协程和异常处理
协程向调用方
协程运行过程中的异常, 会向上冒泡, 传递给next
函数或者send
方法的调用方, 调用方如果没有对其进行处理, 就会导致错误终止.
复制 @coroutinue
def averager():
# 使用协程求平均值
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count
复制 coro_avg = averager()
print(coro_avg.send(40))
print(coro_avg.send(50))
print(coro_avg.send('123')) # 由于发送的不是数字,导致内部有异常抛出。
结果为:
复制 40.0
45.0
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-23-0aa2c396e8a8> in <module>()
2 print(coro_avg.send(40))
3 print(coro_avg.send(50))
----> 4 print(coro_avg.send('123')) # 由于发送的不是数字,导致内部有异常抛出。
<ipython-input-22-f96454dd016a> in averager()
7 while True:
8 term = yield average
----> 9 total += term
10 count += 1
11 average = total/count
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
调用方向协程
上面的例子, 换个角度, 可以看做是传入特别的参数 , 手动使协程退出. 而生成器中有两个特别的方法可以实现这种事情.
throw
方法
generator.throw(exc_type[, exc_value[, traceback]])
, 这个方法使生成器在暂停的yield表达式处抛出指定的异常 , 如果生成器处理了抛出的异常, 代码会继续向下进行, 直到遇到下一个yield表达式或整个代码运行完毕. 当然, 如同send
方法一样, 下一个yield产出的值会成为调用throw方法得到的返回值. 如果没有处理, 则向上冒泡, 直接抛出
close
方法
generator.close()
, 生成器在暂停的yield表达式处抛出GeneratorExit 异常, 如果生成器没有处理这个异常或者抛出了StopIteration异常, 调用方不会报错, 如果收到GeneratorExit异常, 生成器一定不能产出值, 否则解释器会抛出RuntimeError异常.
复制 class DemoException(Exception):
pass
复制 @coroutinue
def exc_handling():
print('-> coroutine started')
while True:
try:
x = yield
except DemoException:
print('*** DemoException handled. Conginuing...')
else:
# 如果没有异常显示接收到的值
print('--> coroutine received: {!r}'.format(x))
raise RuntimeError('This line should never run.') # 这一行永远不会执行
复制 exc_coro = exc_handling()
exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.close()
输出为:
复制 -> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13
查看当前状态:
复制 inspect.getgeneratorstate(exc_coro)
结果为:
如果传入DemoException, 因为做了异常处理, 协程不会中止:
复制 exc_coro = exc_handling()
exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.throw(DemoException) # 协程不会中止,但是如果传入的是未处理的异常,协程会终止
结果为:
复制 -> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13
*** DemoException handled. Conginuing...
查看当前状态:
复制 inspect.getgeneratorstate(exc_coro)
结果为:
协程return
由于协程执行完毕后向外报StopIteration
错误, 对于有return
值的协程生成器, 如何获取它return的值, 是需要考察的问题.
复制 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 # 返回一个namedtuple,包含count和average两个字段。在python3.3前,如果生成器返回值,会报错
return Result(count, average)
复制 coro_avg = averager()
next(coro_avg)
coro_avg.send(20) # 并没有返回值
coro_avg.send(30)
coro_avg.send(40)
# 发送None终止循环,导致协程结束。生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回值。
coro_avg.send(None)
结果为:
复制 ---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-32-e7656e6f4153> in <module>()
5 coro_avg.send(40)
6 # 发送None终止循环,导致协程结束。生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回值。
----> 7 coro_avg.send(None)
StopIteration: Result(count=3, average=30.0)
可以看到奇特的一点, return表达式的值, 会作为StopIteration 异常的一个属性值被带出, 这样做是为了保留生成器对象耗尽时抛出StopIteration异常的行为. 如果要获取这个值, 就需要如下的操作:
复制 coro_avg = averager()
next(coro_avg)
coro_avg.send(20) # 并没有返回值
coro_avg.send(30)
coro_avg.send(40)
try:
coro_avg.send(None)
except StopIteration as exc:
result = exc.value
result
就能得到结果:
复制 Result(count=3, average=30.0)
参考资料