生成器实现协程功能
一个简单的实现协程功能的生成器函数
def coro():
n = 0
while n < 100:
print('I love u.')
ask = 'Do u love me?'
answer = yield ask
print('You answer is...', answer)
n += 1
co = coro()
first_query = next(co)
print(first_query)
for i in range(99):
receive = co.send('yes')
print(receive)
这里生成器实现的协程功能和迭代是一点关系都没有的。
上述代码的效果是:调用方和协程方双方,在执行一部分自己的代码后,把代码执行权让渡给对方,同时传递一些信息给对方,然后由对方执行对方的一段代码,对方执行完一段代码后,返回某些信息并将代码执行权归还回来……最终实现了双方任务的交替执行,这种交替执行就是并发的雏形了,虽然从微观的时间点上看,同时只有一段代码在执行,但是从宏观的时间段上,调用方和协程方是“同时”并发执行的。
上面的协程为例:
不考虑第一个next()的话流程很简单:
调用方使用send(x)
,会发送x
并且让出执行权,调用方挂起,直到协程执行了yield
,调用方的send()
接住协程yield出的值,调用方恢复执行,然后下一个send()
又挂起……。
协程使用yield y
会抛出y
给调用方并且让出执行权,协程挂起,直到调用方执行了send()
,yield
语句接住调用方send
过来的值,协程方恢复执行,然后下一个yield
又挂起……。
也就是说:二者通过 send
和yield
互相“传皮球”,交替执行,宏观上达到了同时执行的效果。
这里可以解释为什么调用方第一个要执行next()
了,因为调用send(message)
时会把message赋值给协程的yield
语句,作为yield
语句的返回值,而最开始协程里面还没有执行过yield
语句,有个毛的返回值要获取啊。。。所以只能使用next(co)
或co.send(None)
,二者等价。对一个刚启动的协程调用next()
,使它执行第一个yield
语句,叫做预激。可以使用装饰器在定义协程时实现自动预激。
这只是使用生成器实现的最基础的协程,协程函数中只有yield
。实际上还有一种关键字 yield from
。
yield from是个啥意思?
有两种含义,第一种含义就是一个单纯的语法糖,yield from iterable
,效果是把这个可迭代对象中的每个元素都yield出去,和写一个for遍历挨个yield没啥区别,也就是少写一行代码。
如果只是为了少写一行,完全没有必要新增一个关键字。要是有人问你yield from是啥意思,你单纯说个语法糖跟没说一样。
yield from还有另一种语义,like this:
result = yield from corotine
yield from后面跟着另一个协程。
这里yield from 实际上起到的是一个信息通道、异常捕获的作用。要说明这一场景需要引入三个概念。
调用方:最外层的caller,可以理解为main()函数
委派协程:由调用方驱动的,内部包含yield from subcorotine
语句的协程
子协程:就是上一行里的subcorotine
,一个仅包含yield的最简单的协程。
这里的子协程可否也是一个委派协程?当然可以,但是整个协程链的终点一定要是一个最简单的yield
基础款协程。
这个执行过程中:
调用方通过send()驱动委派协程,委派协程执行到yield from时,yield from 的内部实现其实也包含了多个yield,委派协程从调用方手中拿到send()发来的信息m,转手自己又调用一次send()把m原封不动地发给子协程,来驱动子协程运行,子协程yield产出的值,委派协程又原封不动地yield给调用方,最终实现的效果是一个“信息通道”,调用方本质上驱动的是子协程,我们可以认为委派协程在yield from 的右侧挂起了,但我们知道它实际上是在运行的——一直在忠实地履行“传话筒”的职责。
当子协程执行完毕时,yield from
语句会捕获子协程抛出的StopIteration
异常,从中抓取到子协程的返回值,赋值给= yield from
等号左边的变量,委派协程从阻塞中恢复,它可以自己执行一些yield工作,也可以再执行一段yield from
委派工作给另一个新的子协程。
那么我们现在可以解释yield from
是干嘛的了:
当协程corotine1
执行了result = yield from corotine2
,
我们可以认为corotine1
就此挂起,把执行权交给了corotine2
,
直到corotine2
执行完毕,corotine1
被唤醒,yield from
捕获异常抓取到corotine2
的结果,赋值给等号左边的result。实际上corotine2
中又可以再yield from
新的子协程,可以说yield from
让一个协程本身也可以作为一个调用方来驱动新的协程,并帮你自动获取协程子任务的执行结果。