协程可以看做加强版生成器,这从协程定义的文档名称就可以看出,协程的底层架构在“PEP 342—Coroutines via Enhanced Generators”中定义。
定义在Python 2.5(2006 年)实现了。自此之后,yield 关键字可以在表达式中使用,而且生成器 API 中增加了 .send(value) 方法。生成器的调用方可以使用 .send(...) 方法发送数据,发送的数据会成为生成器函数中 yield 表达式的值。因此,生成器可以作为协程使用。
自Python 3.3(2012 年)实现的“PEP 380—Syntax for Delegating to a Subgenerator”对生成器函数的句法做了两处改动,以便更好地作为协程使用:
- 生成器可以返回一个值;以前,如果在生成器中给 return语句提供值,会抛出 SyntaxError 异常。
- 3新引入了 yield from 句法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去了之前把生成器的工作委托给子生成器所需的大量样板代码。
以下所有案例代码均在《流畅的python》第16章的案例基础上做了修改。
案例1:
def simple_coroutine():
print('coroutine started')
x = yield
print('coroutine received:',x)
if __name__=='__main__':
gen = simple_coroutine()
print(gen)
next(gen)
gen.send(42)
代码运行结果:
<generator object simple_coroutine at 0x000000F033696AF0>
coroutine started
coroutine received: 42
Traceback (most recent call last):
...
StopIteration
案例分析:
第一个案例比较直观的演示了协程的用法。通过代码可以看到此时yield 在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是 None——这个值是隐式指定的,因为 yield 关键字右边没有表达式。
与创建生成器的方式一样,调用函数得到生成器对象。打印变量gen可以验证此时引用的确实是一个生成器对象。此时生成器还没启动,无法发送数据,需要先调用一遍 next(...) 函数,这称为协程的预激(prime,所谓预激就是让协程的代码执行到第一个 yield 表达式位置)。 此时代码会在yield所在表达式位置挂起。这里需要特别注意,一个赋值语句是分为三步的,即声明变量并开辟内存空间,计算赋值运算符右侧的表达式值,计算完毕后将值赋给赋值运算符左侧的变量。协程中代码是在赋值语句的最后一步挂起,也就是赋值运算符右侧的表达式已经计算完毕,就要进行给x赋值时代码挂起。此时,右侧表达式的计算结果并不是准备赋值给左侧的x的,而是返回到了调用者位置(也就是调用next(gen)的位置),而真正给x赋值的内容是接下来通过send方法传入的内容。本例中比较特殊之处在于,赋值语句右侧没有表达式,那么返回到next(gen)位置的值将是None。调用send(42)方法后,yield表达式利用传入的42为x赋值。现在协程会恢复,并一直运行到下一个 yield 表达式位置或者运行到结尾。当代码运行到协程定义体的末尾,导致生成器像往常一样抛出 StopIteration 异常。
对于协程如果没有经过预激就开始调用send方法生成值,会产生如下异常:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
案例2:
def simple_coroutine(a):
print('coroutine start, a: ',a)
b = yield a
print('receive b: ',b)
c = yield a+b
print('received c: ',c)
if __name__=='__main__':
gen = simple_coroutine(14)
print(next(gen))
print(gen.send(28))
gen.send(99)
运行结果:
coroutine start, a: 14
14
receive b: 28
42
received c: 99
Traceback (most recent call last):
...
StopIteration
案例分析:
调用next(gen)是完成预激,让代码运行到第一个yield位置挂起,根据案例一的分析,挂起是在赋值运算符右侧表达式运算完毕准备进行赋值操作时挂起,而且根据案例一的分析还知道,右侧表达式运算结果是返回到调用处(而不是用来给赋值运算符左侧的变量真正赋值的),此时右侧表达式计算结果为14(也就是参数a的值),返回到了调用next(gen)的地方被打印到了控制台。当调用send(28)的时候,协程代码复活,利用传入的28完成了赋值语句,并继续运行到下一个yield位置挂起。挂起时赋值运算符右侧的表达式a+b的计算结果42被返回到了send(28)调用处,42被打印到了控制台上。随后调用send(99),协程代码复活利用99完成了变量c的赋值,并继续运行到协程代码的终止处产生StopIteration异常。
案例3:
def myavg():
total = 0.0
count = 0
result = None
while True:
temp = yield result
total += temp
count += 1
result = total / count
if __name__=='__main__':
gen = myavg()
next(gen)
print(gen.send(10))
print(gen.send(20))
print(gen.send(60))
运行结果:
10.0
15.0
30.0
案例分析:
调用next(gen),代码运行到第一个yield位置,此时右侧表达式的值None返回到了调用处,如果print(next(gen))就会看见一个None。send(10)让赋值语句完成,temp为10,协程代码继续运行到直到下一次yield处,挂起时将计算得到result返回到send(10)调用处,因此10.0被打印到了控制台。继续send(20),协程代码复活,temp赋值为20,result计算为15,在yield处挂起时将计算结果15返回到调用处被打印到控制台。继续send(60),协程代码复活,temp赋值为60,result计算为30,在yield处挂起时将计算结果30返回到调用处被打印到控制台。
通过以上3个案例可以看到协程使用前必须要经过预激,否则会产生错误。如何有效的避免忘记预激呢?另外,第3个案例中,while循环会一直处于工作状态,如何让协程终止呢?继续看下面的案例。
案例4
利用装饰器完成协程的预激。
def mywrap(func):
def temp(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return temp
mywarp是一个装饰器函数,调用mywarp函数获得temp函数,向temp函数中传入func(也就是真正的协程函数)所需要的参数后获得预激好的生成器。
使用方式如下:
def mywrap(func):
def temp(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return temp
def myavg():
total = 0.0
count = 0
result = None
while True:
temp = yield result
total += temp
count += 1
result = total / count
if __name__=='__main__':
wrap = mywrap(myavg)
gen = wrap()
print(gen.send(10))
print(gen.send(20))
print(gen.send(60))
也可以使用装饰器的语法糖:
def mywrap(func):
def temp(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return temp
@mywrap
def myavg():
total = 0.0
count = 0
result = None
while True:
temp = yield result
total += temp
count += 1
result = total / count
if __name__=='__main__':
gen = myavg()
print(gen.send(10))
print(gen.send(20))
print(gen.send(60))
使用后面介绍的yield from句法可以自动预激子协程,因此使用yield from与mywrap装饰器就会产生冲突。
案例5
利用throw()或close()方法产生指定的异常终止协程的运行。
throw(异常类型):
在暂停的yield表达式处抛出指定的异常。如果生成器利用try...except...处理了该异常,代码会执行到下一个yield处。如果生成器自身没有处理该异常,该异常会向上抛出到调用方。
close():
在暂停的yield表达式处抛出GeneratorExit异常。注意:如果生成器没有处理这个异常或抛出了StopIteration异常(也就是yield表达式从挂起处恢复后执行到了结束),调用方不会有报错!但如果收到了GeneratorExit异常后生成器还在产出值,则解释器抛出RuntimeError异常。
class DemoException(Exception):
pass
def demo_exc_handling():
print('-> coroutine started')
while True:
try:
x = yield
except DemoException:
print('handle DemoException!')
else:
print('get ',x)
raise Exception('???')
if __name__=='__main__':
gen = demo_exc_handling()
next(gen)
gen.send(10)
gen.throw(DemoException())
gen.send(20)
gen.close()
运行结果:
-> coroutine started
get 10
handle DemoException!
get 20
案例分析:
创建生成器后通过预激,协程代码运行到x=yield处挂起,此时控制台上打印->coroutine started,代码在x=yield处挂起,yield右侧的变量表达式返回None到调用处。执行gen.send(10)时,yield表达式恢复运行并将传入的10赋值给x,此时没有异常产生于是执行else中的代码,在控制台输出get 10,继续下一轮循环,返回None并在yield赋值时挂起。执行gen.throw(DemoException),此时yield恢复执行并抛出DemoException,异常会被except捕获,在控制台输出handle DemoException后,代码继续执行到yield表达式赋值处,返回None到调用处继续挂起。然后执行send(20)与send(10)过程类似,最后执行close(),在yield表达式挂起处抛出GeneratorExit异常,根据前面的描述,该异常未被捕获,所以调用方并没有接收到任何报错。且协程由挂起状态转为关闭状态。
协程一共有四种状态,分别是等待开始执行的GEN_CREATED,解释器正在执行状态GEN_RUNNING,在yield表达式处挂起状态GEN_SUSPEND,和执行结束状态GEN_CLOSED。查看协程的状态使用inspect.getgeneratorstate函数即可。
我们稍微修改下案例5,插入inspect.getgeneratorstate函数观察协程gen的状态变化:
if __name__=='__main__':
gen = demo_exc_handling()
print(inspect.getgeneratorstate(gen))
next(gen)
print(inspect.getgeneratorstate(gen))
gen.send(10)
gen.throw(DemoException())
gen.send(20)
gen.close()
print(inspect.getgeneratorstate(gen))
运行结果:
GEN_CREATED
-> coroutine started
GEN_SUSPENDED
get 10
handle DemoException!
get 20
GEN_CLOSED
在创建gen对象之后但激活之前,gen的状态为GEN_CREATED,预激之后,协程在yield表达式赋值处挂起,因此gen的状态为GEN_SUSPENDED,最后在执行完close()后,协程关闭,状态为GEN_CLOSED。
如果传入了生成器无法处理的异常,异常会被抛出,且协程终止。
案例6
让协程有返回值。
python3.3版本之前,在协程中出现return语句会产生错误。
修改myavg函数,让其有一个返回值:
Result = namedtuple('Result','count average')
def myavg():
total = 0.0
count = 0
average = None
while True:
temp = yield
if temp is None:
break
total += temp
count += 1
average = total / count
return Result(count,average)
if __name__=='__main__':
gen = myavg()
next(gen)
gen.send(10)
gen.send(20)
gen.send(30)
gen.send(None)
注意协程的循环体有改变,当temp的值为None的时候,协程就会运行终止。随着协程运行终止就会抛出StopIteration异常。而生成器返回的结果Result对象将作为抛出的StopIteration对象的value属性值被提交到调用处。如果为了获取这个返回值,需要捕获抛出的StopIteration异常然后获取其value属性值:
if __name__=='__main__':
gen = myavg()
next(gen)
gen.send(10)
gen.send(20)
gen.send(30)
try:
gen.send(None)
except StopIteration as e:
result = e.value
print(result)
代码运行结果:
Result(count=3, average=20.0)
这样设计的目的主要是为了保证协程结束时抛出StopIteration的机制。
python的yield from可以像for循环那样在语句内部处理掉StopIteration异常并获得协程的返回值。看下面的例子。
案例7
使用yield from。
yield from与for类似也可以内部处理StopIteration异常,先看yield from替代for的用法:
def myfor():
for ch in 'ABC':
yield ch
for i in range(1,3):
yield i
def myyieldfrom():
yield from 'ABC'
yield from range(1,3)
if __name__=='__main__':
print(list(myfor()))
print(list(myyieldfrom()))
运行结果:
['A', 'B', 'C', 1, 2]
['A', 'B', 'C', 1, 2]
然后yield from的作用不仅于此,yield from的作用是可以获得一个委派生成器,但是这个委派生成器本身不处理传入的内容,而是像一个通道,将客户端利用派生生成器传入的值直接传递给子生成器,当子生成器处理完数据协程关闭,并随StopIteration将结果返回的时候,委派生成器会处理StopIteration获得value属性的值也就是子生成器的结果。看案例:
data = {
'girls;kg':
[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m':
[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg':
[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m':
[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}
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)
def grouper(results, key):
while True:
results[key] = yield from averager()
def main(data):
results={}
for key,values in data.items():
group = grouper(results,key)
next(group)
for value in values:
group.send(value)
group.send(None)
report(results)
def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))
if __name__=='__main__':
main(data)
运行结果:
9 boys averaging 40.42kg
9 boys averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m
案例分析:
data为某幼儿园10名女生和9名男生的生身高和体重信息。现在要求出他们的平均值。案例中的grouper函数带yield from语句,因此它是一个委派生成器,执行grouper()其返回值就是委派生成器。在main方法中,当执行完语句group = grouper(results,key)之后,就获得了委派生成器group,随即next(group)是在预激。随后在for value in values中,调用group.send(value)时,就是将值源源不断的提交给子生成器。谁是子生成器?grouper函数中,yield from后面跟的是myavg函数的返回值,myavg返回值是一个生成器,也就是此时委派生成器的子生成器。
具体的执行步骤就是,next(group)时,委派生成器挂起在yield from表达式赋值处。随后for value in values循环中,不断通过group.send出来的值都直接交给子生成器处理。整个传值过程中group一直处于挂起状态。当values中的数据传递完毕(例如10个女孩的体重数据),此时循环结束,马上执行的是group.send(None),子生成器收到None离开结束协程,抛出StopIteration异常并用value属性携带者子生成器的返回值。此时yield from恢复运行,处理StopIteration异常拿到value属性值并赋值给results[key]。然后继续运行到写一个yield from处。此时myavg函数再次运行生成了新的子生成器,group挂起。但是在main方法中,当group.send(None)完毕后会执行外层循环的下一个循环,下一个循环一开始就调用了grouper函数,这样就重新新建了一个委派生成器group,然后继续通过该group传递数据到子生成器。
因此每次处理新的一组数据时,都会新建一个委派生成器,委派生成器将该组组内的所有数据都交给子生成器处理,当该组数据处理完成后,派生生成器发送None,子生成器产生StopIteration异常并返回结果,yield from恢复运行,处理StopIteration异常,拿到结果赋值给results[key]。