老实说,这部分内容是我学习Python以来遇到的最大挑战,堪比以前学习多线程时候的经历,有种脑袋要爆炸的感觉。
废话不多说了,GO!
yield与生成器函数
前边在中我们介绍了生成器函数,生成器函数本质上是通过yield
语句来产生一个值提供给调用程序,然后挂起,并等待下一次调用,不断执行这一个过程的特殊函数。
这其中yield
语句除了用于向调用方生成数据以外,还肩负着控制生成器函数内的执行流程的作用,毕竟当yield
语句被执行后,控制流程就会转到调用程序,生成器函数会被挂起。
这就很有意思了,而更有意思的是,yield
不仅仅能产生数据,还能接收数据。
而接收数据,恰恰就是生成器函数向协程进化的一大关键,我们先来看调用程序如何向生成器函数“动态”传递数据。
传入数据
我们看一个简单的例子:
def simpleCoroutine():
i = yield
print("coroutine received {!s}".format(i))
sc = simpleCoroutine()
next(sc)
print("coroutine wailt")
sc.send(11)
# coroutine wailt
# coroutine received 11
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note33\test.py", line 8, in <module>
# sc.send(11)
# StopIteration
在这个例子中,先创建了一个生成器函数实例sc
,然后通过执行next(sc)
让程序执行生成器函数simpleCoroutine
,并且在yield
语句处挂起。
不同于之前介绍生成器函数时候的例子,这里的yield
语句后没有跟任何变量,所以此时不会产生任何值,如果此时接收next
产生的值,将获得一个None
。
而且很容易注意到,此时的yield
前面还有一个赋值语句。这时候我们在外部的调用程序中就可以使用sc.send
的方式将数据传入,传入后的生成器函数会通过赋值语句获取数据,并继续执行。当然在这个简单示例中是执行到尾部然后抛出StopIteration
异常,这点和之前我们介绍的生成器函数的特点完全一致。
现在生成器函数不仅仅是单向产生数据了,还能接收数据,这样的生成器函数我们称之为协程。
下面我们系统介绍一下协程。
协程
协程,对应的单词是Coroutine,意思是协同程序。总的来说,协程很像是单线程中的一种“子程序”,所以它经常用来用来进行一些仿真程序,通过设置多组协程来模拟某种场景下的事件驱动等仿真结果。
状态
协程在运行的时候会有一些状态,这里通过一个简单程序说明:
import inspect
def simpleCoroutine():
print("coroutine start")
print("coroutine running")
print("coroutine wait")
i = yield
print("coroutine running")
def showCoroutineStatus(coroutine):
print("coroutine status is {!s}".format(inspect.getgeneratorstate(coroutine)))
sc = simpleCoroutine()
showCoroutineStatus(sc)
print("start coroutine")
next(sc)
showCoroutineStatus(sc)
print("send data to coroutine")
try:
sc.send(11)
except StopIteration:
pass
showCoroutineStatus(sc)
# coroutine status is GEN_CREATED
# start coroutine
# coroutine start
# coroutine running
# coroutine wait
# coroutine status is GEN_SUSPENDED
# send data to coroutine
# coroutine running
# coroutine status is GEN_CLOSED
这里使用inspect.getgeneratorstate
获取协程状态。
刚创建的协程实例sc
处于GEN_CREATED
状态,我们通过next(sc)
可以对协程进行"激活",此时协程会进入运行状态,直到执行到第一个yield
处进行挂起,此时协程将处于GEN_SUSPENDED
状态,然后我们通过sc.send(11)
给协程传入数据,协程就从挂起状态恢复为运行状态,直到运行结束后抛出StopInteration
异常。此时协程处于GEN_CLOSED
状态。
实际上协程状态还有GEN_RUNNING
,但因为这里是单进程程序,所以并不能观察到此状态,所以只要清楚协程内部执行的时候协程是处于运行状态就可以了。
除了说明协程状态,这个示例还展示了如何使用next
对协程进行激活以及使用send
传递数据给协程。
使用
send(None)
也可以激活协程,但需要注意的是一定要传入None
,如果试图对GEN_CREATED
状态的协程传入非None
值进行激活,会产生异常。
此外我们还可以给协程传入异常或者显式结束协程。这里我们使用一个计算动态平均数的程序进行说明。
动态平均数
import inspect
import random
def coroutineAverager():
total = 0.0
count = 0
result = 0.0
while True:
newNumber = yield result
total += newNumber
count += 1
result = total/count
avg = coroutineAverager()
next(avg)
numbers = [random.randint(1, 20) for i in range(5)]
countedNums = []
for i in numbers:
result = avg.send(i)
countedNums.append(i)
print(countedNums, "average:", result)
avg.close()
print(inspect.getgeneratorstate(avg))
# [17] average: 17.0
# [17, 2] average: 9.5
# [17, 2, 4] average: 7.666666666666667
# [17, 2, 4, 7] average: 7.5
# [17, 2, 4, 7, 9] average: 7.8
# GEN_CLOSED
这里展示了一个用于计算动态平均数的协程,协程中通过一个死循环来不断读取数据,并在计算后返回平均值。调用程序在计算完所有数据的平均数后调用close
来显式关闭协程。
值得注意的是,这里的
yield
语句相当于完全体,它会将右侧的表达式计算的值生产给外部的调用程序,并且在挂起后会等待调用程序传入一个值传递给左侧的赋值语句,这里的yield
相当于协程和外部的门户,同时承担数据的产生和接收。这里使用图示的方式说明:
在协程中,每次都会在红线标示的部分挂起并等待调用方传入数据。
事实上在调用close
的时候,解释器会传递给协程一个GeneratorExit
异常,协程会在挂起的yield
语句处抛出此异常,然后解释器捕获该异常并结束掉协程。
这里展示用异常来控制关闭协程:
import inspect
import random
def coroutineAverager():
total = 0.0
count = 0
result = 0.0
while True:
try:
newNumber = yield result
except GeneratorExit:
break
total += newNumber
count += 1
result = total/count
avg = coroutineAverager()
next(avg)
numbers = [random.randint(1, 20) for i in range(5)]
countedNums = []
for i in numbers:
result = avg.send(i)
countedNums.append(i)
print(countedNums, "average:", result)
# avg.close()
try:
avg.throw(GeneratorExit)
except StopIteration:
pass
print(inspect.getgeneratorstate(avg))
# [4] average: 4.0
# [4, 15] average: 9.5
# [4, 15, 14] average: 11.0
# [4, 15, 14, 7] average: 10.0
# [4, 15, 14, 7, 1] average: 8.2
# GEN_CLOSED
如果我们是通过throw
来传入GeneratorExit
异常,就需要在协程中的yield
语句处捕获并处理异常,并且在退出循环体后会向上抛出StopIteration
异常,要在调用程序中处理。
对于这个例子,其实可以通过一种更简单的方式让协程退出,比如通过send
传递一个不太常见的值,作为关闭标识,比如None
:
import inspect
import random
def coroutineAverager():
total = 0.0
count = 0
result = 0.0
while True:
newNumber = yield result
if newNumber is None:
break
total += newNumber
count += 1
result = total/count
avg = coroutineAverager()
next(avg)
numbers = [random.randint(1, 20) for i in range(5)]
countedNums = []
for i in numbers:
result = avg.send(i)
countedNums.append(i)
print(countedNums, "average:", result)
try:
avg.send(None)
except StopIteration:
pass
# [13] average: 13.0
# [13, 8] average: 10.5
# [13, 8, 17] average: 12.666666666666666
# [13, 8, 17, 18] average: 14.0
# [13, 8, 17, 18, 5] average: 12.2
但此时已然需要调用程序处理StopIteration
异常,对比一下发现还是调用close
更方便。
return
直到现在为止,协程还只是通过yield
语句将内部产生的数据传递给调用程序,事实上现在Python也可以通过return
将协程内的数据返回给外部程序。
import inspect
import random
def coroutineAverager():
total = 0.0
count = 0
result = 0.0
while True:
newNumber = yield
if newNumber is None:
break
total += newNumber
count += 1
result = total/count
return result
avg = coroutineAverager()
next(avg)
numbers = [random.randint(1, 20) for i in range(5)]
for i in numbers:
avg.send(i)
try:
avg.send(None)
except StopIteration as e:
print(numbers)
print("avg:", e.value)
# [3, 18, 10, 9, 13]
# avg: 10.6
可以看到,外部程序是通过StopIteration
异常实例的value
属性获取到协程返回的值,这或许有点怪异,但协程的确是通过把返回值赋值给StopIteration.value
的方式传递的。
使用装饰器进行激活
我们在前面已经说过了,使用协程之前要先用next
或者send(None)
的方式对协程进行激活,让协程处于GEN_SUSPECT
状态,这时候才可以传递数据进行进一步使用。
除了每次都next
以外,我们还有一个额外选项,构建一个进行“预激活”的装饰器,用这个装饰器来装饰协程,这样我们就不需要进行“手动激活”了。
import inspect
def coroutineDecorator(func):
import functools
functools.wraps(func)
def coroutineWrap(*avgs, **kwAvgs):
coroutine = func(*avgs, **kwAvgs)
next(coroutine)
return coroutine
return coroutineWrap
def simpleCoroutine():
i = yield
print("receive {!s}".format(i))
sc = simpleCoroutine()
print(inspect.getgeneratorstate(sc))
try:
sc.send(11)
except StopIteration:
pass
print(inspect.getgeneratorstate(sc))
需要说明的是,虽然使用这种装饰器进行“预激活”很方便,但是有一些Python内建组件,比如后边会展示的yield from
语句本身就具有“预激活”装饰器的作用,此时就不能使用我们自定义的“预激活”功能,否则就会出问题。
yield from
原理
我们在中简单介绍过yield from
语句,这个语句最浅显的用途是可以将生成器函数中遍历迭代器的功能“委托”给迭代器并直接生成数据提供给调用程序:
def chain(*iterators):
for iterator in iterators:
yield from iterator
l = list(chain(range(5),"abc"))
print(l)
# [0, 1, 2, 3, 4, 'a', 'b', 'c']
事实上,yield from
比表面上的用途要复杂的多,yield from
的真实运作机制其实是将外部的调用程序和协程中的子协程直接“连通”,此时的协程相当于一个委托程序,外部调用程序通过调用委托协程的next
和send
来直接调用到委托程序中的子协程的相应方法,并且通过委托协程中转后直接获得生成的数据。
计算多组动态平均数
下面通过一个示例程序进行说明:
import pprint
import random
def coroutineAverager():
total = 0.0
count = 0
result = 0.0
while True:
newNumber = yield
if newNumber is None:
break
total += newNumber
count += 1
result = total/count
return result
def dealDataCoroutine(result, kind):
result[kind] = yield from coroutineAverager()
data = {}
data["boy_height"] = [random.randint(160, 195) for i in range(10)]
data["girl_height"] = [random.randint(150, 185) for i in range(10)]
data["boy_weight"] = [random.randint(60, 100) for i in range(10)]
data["girl_weight"] = [random.randint(40, 80) for i in range(10)]
result = {}
for kind, kindData in data.items():
ddc = dealDataCoroutine(result, kind)
next(ddc)
for item in kindData:
ddc.send(item)
try:
ddc.send(None)
except StopIteration:
pass
pprint.pprint(data)
print(result)
# {'boy_height': [189, 192, 174, 163, 160, 171, 173, 170, 194, 186],
# 'boy_weight': [79, 78, 76, 98, 86, 81, 78, 68, 97, 68],
# 'girl_height': [165, 170, 167, 169, 174, 177, 161, 154, 177, 167],
# 'girl_weight': [51, 52, 65, 46, 78, 71, 75, 63, 58, 65]}
# {'boy_height': 177.2, 'girl_height': 168.1, 'boy_weight': 80.9, 'girl_weight': 62.4}
这里利用了前面计算动态平均数的协程,并使用一个委托协程dealDataCoroutine
进行调用。
测试数据是四组男女同学的身高和体重。
首先我们对于每组数据,通过ddc = dealDataCoroutine(result, kind)
创建一个委托协程,并且使用next(ddc)
进行激活。
需要注意的是,激活委托协程以后,委托协程的yield from
语句会将委托协程挂起,并且激活子协程coroutineAverager()
,事实上此时子协程已经执行到newNumber = yield
语句并挂起,并等待外部程序传递数据。此后外部程序所有的next
和send
调用都将直接作用于子协程,所以ddc.send(item)
是直接传递数据给计算动态平均数的子协程进行累积计算,这期间委托协程一直处于挂起状态。一直到该组数据全部传递完毕,我们调用了send(None)
,此时子协程将跳出内部循环,并将return
返回的值附加给StopIteration
异常,然后抛出。幸运的是这种情况下yield from
语句可以自动捕获StopIteration
异常,并且将其value
属性赋值给前边的result[kind]
,所以在委托协程中我们没有显式处理StopIteration
异常。但是当委托协程解除挂起并执行完赋值语句后,就会退出,并且抛出StopIteration
,所以我们在外部程序需要处理该异常。
这一段是最难理解,也是最核心的内容,比较烧脑,建议结合代码多读几遍。
其他几组数据的执行过程与上边所说的完全一致,只不过是新创建了一个委托协程。
我绘制了一张时序图,可能会对理解有所帮助:
可以看到,当作为委托协程的dealDataCoroutine
创建并激活了子协程coroutineAverager
后,子协程和主程序就直接建立了联系,此后就是主程序和子协程直接交互,期间委托协程都处于挂起的状态,直到子协程收到特定标识退出内部循环并返回StopIteration
异常,此时委托协程恢复运行,并存入关键数据,然后同样向主协程抛出异常。
这里需要指出的是,时序图只是展示了逻辑上的交互,事实上委托协程之所以可以以隐形的方式上下沟通主程序和子协程,这完全是
yield from
语句的功劳,它涵盖了所有接收主程序指令并调用子协程然后向主协程返回信息的所有功能,此外还可以正确处理各种异常,并且还可以获取子协程通过return
返回的数据。
PEP-492
协程这一技术在伴随着Python的版本更迭不断发生变化,在中,可以用新的关键字创建协程(已经在Python3.5实装):
async def read_data(db):
pass
并搭配新的关键字来控制协程,这点等我阅读完PEP-492后再来补全。
今天花了点时间阅读并翻译了,感兴趣的可以移步阅读。
老实说,相比《Fluent Python》原文,这里的说明相当简单,我不知道能否阐述明白协程的运作机制,
事实上使用时序图应该对理解yield from
很有帮助,这点我需要思考一下如何绘制。
目前就这样了,有空会完善这篇有难度的文章,谢谢阅读。
本系列文章的代码都存放在Github项目:。
文章评论