前面一篇关于Python协程的文章中,提到了python中协程的一些历史以及一些简单的用法,这些用法放在实际开发中可能不太够,我在后面的编写中就踩到了不少的坑,这里就回应上篇文章中末尾说的,再写一篇文章来梳理一下这些问题。时隔很久,终于还是磨磨唧唧的把第二篇写出来了。本来还准备有第三篇重生篇的,但是以防拖稿,合并到一起来写吧。0x00 又是生成器?我们已经知道了,生成器可以作为协程来使用,但是很多细节问题尚未明确,我们先从最简单的一个协程开始。(后面会出现"生成器"和"协程"两个词混用的问题,可以都理解为"作为协程使用的生成器")
def easy_coroutine(): print("Start!") x = yield print("End! x: {}".format(x))
这就是最简单的协程了,如果想使用这个协程,看起来还是有点麻烦的:
$ python -i .z_coroutine.py >>> easy_coroutine >>> c <generator object easy_coroutine at 0x0000016AD411E570> >>> nextc Start! >>> c.send666 End! x: 666 Traceback most recent call last: File , line 1, in <module> StopIteration >>>
创建协程的方式与创建生成器一样,通过调用函数的方法获取到一个生成器对象。紧接着调用next()方法来启动生成器,这一步也称为prime,有些文章会把这个东西翻译成"预激",即让协程开始执行到第一个yield表达式的位置。(为了方便下文都称为预激了)
下面又调用了c.send(666),调用这个方法后,x = yield表达式就会得到666这个值,然后协程恢复运行,直到协程结束或者遇到另外的yield表达式为止。当然这里最后会抛出一个StopIteration异常,因为代码执行完毕了,这就是生成器的标准行为,不用在意。
在这个执行过程中,会发现我们的easy_coroutine()经历的多个状态,并非总是一直处于RUNNING的状态。事实上,协程的状态有四种:GEN_CREATED,GEN_RUNNING,GEN_SUSPENDED,GEN_CLOSED。这四种状态分别是等待开始执行,正在执行,停在了yield表达式位置,执行结束。如果你想获取某个协程的当前状态,可以通过inspect.getgeneratorstate函数获取。
额外说明一点,send()方法的参数会传递给协程,作为yield表达式的值,所以只有当协程处于GEN_SUSPENDED的状态时,才可以调用send()方法传递值。如果协程还没有prime,那么可以通过调用send(None)来预激协程。啥,你问为什么不能通过给尚未prime的协程调用send(233)来预激协程,如果你试过的话就会收到一个TypeError。
每次预激协程的时候都需要调用一遍next()函数,很麻烦,而且很容易忘掉,一旦忘记预激协程,后果也是十分严重的,因为没有被预激的协程没有任何作用。有没有简单的方法让Python自动预激协程呢?有的,函数装饰器就可以做到这一点,于是我们可以自己封装一套来让其自动预激!
def prime_coroutine(func): @functools.wraps(func) def prime(*args, **kwargs): t = func(*args, **kwargs) next(t) return t return prime
代码十分简单,如果看不懂的话,建议先去学习一下Python的装饰器部分。可能各位很容易想到之前提到过的一种装饰器,在Python3.4以后,标准库中新增了asyncio.coroutine装饰器,用于修饰协程函数,但是非常遗憾,这个装饰器并不会预激协程,而通过yield from语法调用一个协程时,会自动预激。所以可以通过asyncio.coroutine和yield from配合使用,实际上我们也是这样使用的。
这里有人会问,如果在协程中抛出了异常怎么办,是不是和普通函数的异常一样?当协程中出现异常时,而协程本身又没有捕获处理,那么这个异常会"冒泡"至协程的调用方,即next()或send()函数的调用方。如果协程已经抛出了异常,再尝试去调用它,会得到一个StopIteration异常。于是有人发现了可以通过让协程抛出异常的方式终止一个协程的运行。确实没错,而且我们也确实可以通过generator.throw()和generator.close()显示的向协程发送异常,使协程停止。这里举个例子:
def exception_handler_example(): print("Start!") while True: try: x = yield except TypeError: print("Receive type error!!") else: print("Receive value: {}".format(x)) print("End!")
看一下运行样例
$ python -i .z_coroutine.py >>> exception_handler_example >>> nextc Start! >>> c.send666 Receive value: 666 >>> c.sendTypeError Receive value: <class > >>> c.send777 Receive value: 777 >>> c.close >>> c <generator object exception_handler_example at 0x000001499EECEA40> >>> from inspect import getgeneratorstate >>> getgeneratorstatec >>>
这个协程是一个死循环,不停的接受调用方的值并且打印出来,有三个地方需要留意一下。
-
最后的End字符串始终没有打印出来。因为只有没有被捕获的异常会终止这个死循环,然而矛盾的是如果出现了未处理的异常,那么协程就挂了,所以不会输出最后的End字符串。
-
当调用c.close()时,会使生成器的yield表达式部分抛出一个GeneratorExit异常。如果生成器本身没有处理这个异常,或者运行到了生成器的结尾(即抛出了StopIteration)的情况下,调用方不会出现任何错误。另一方面,如果生成器收到了GeneratorExit异常,那么生成器就无法生成值!
当c.close()后,我们获取协程的状态,确实是已经处于执行结束的状态了。
如果调用c.throw()方法,则会在yield表达式那里抛出一个指定的异常,如果生成器捕获并且处理这个异常,那么生成器会继续向下执行到下一个yield表达式的位置。如果生成器没有捕获这个异常,那么异常就会向上"冒泡",传递给生成器的调用方。
0x01 yield进化!
前面提到过,Python后来添加了yield from语法,之所以要引入这种东西,有两种原因,"把异常传入嵌套协程问题"和"让协程更好的处理返回值问题"。
可能有人已经发现了,我们定义的协程并不会return值,只会通过yield生产值。我们下面写个例子,这个协程不会"产出"值,而是"返回"值。
def sum(): total = 0 nums = [] while True: x = yield if not x: break total += x nums.append(x) return total, nums
运行一下看看。
$ python -i .z_coroutine.py >>> co_sum >>> nexts >>> s.send666 >>> s.send233 >>> s.send1 >>> s.sendNone Traceback most recent call last: File , line 1, in <module> StopIteration: 900, 666, 233, 1 >>>
我们通过发送None来终止这个协程的运行,虽然抛出了StopIteration异常,但是异常对象的value部分存有return语句的返回值。我们稍微修改一下调用方法,让这段代码看起来不那么奇怪:
$ python -i .z_coroutine.py >>> co_sum >>> nexts >>> s.send1 >>> s.send2 >>> s.send3 >>> try: ... s.sendNone ... except StopIteration as e: ... e.value ... >>> result 6, 1, 2, 3 >>>
虽然看起来不是很奇怪了,但是从异常对象中获取协程的返回值显得有点难过。为啥这么做呢?我们稍微考虑一下,生成器执行结束后的常规行为是什么?抛出StopIteration异常!那么要想办法把值从协程内部传出来,似乎唯一的方式就是放在异常对象的值中了。更难过的是,这是PEP 380定义的方式。前面提到过,yield from语法结构要解决的问题之一就是"让协程更好的处理返回值问题",所以"yield from"的一个行为就是会在内部自动捕获StopIterator异常,并且将异常对象的value属性设置为yield from表达式的值。
如果你以为yield from的作用就这些的话,那可是大错特错了,它比yield的功能更加强大,作用也多很多。它的主要功能类似于一个"通道"或者一个"委托器",用于将协程的调用方和嵌套协程最内层的子生成器连接起来,让二者可以愉快的发送和生产值。PEP 380中添加了一些新的术语,我们沿用一下。
-
委托生成器(delegating generator):包含yield from <iterable>结构的生成器函数。
-
子生成器(subgenerator):从yield from表达式中iteratable部分获取的生成器。(有时候就叫iterator,23333)
我们来看一个使用了yield from的例子,看起来有点复杂,而且很难受,因为是为了使用yield from刻意编出来的代码。
final_result = {} def co_sum(): total = 0 nums = [] while True: x = yield print("co_sum receive: ", x) if not x: break total += x nums.append(x) return total, nums def middle(key): idx = 0 while True: print("middle idx: ", idx) final_result[key] = yield from co_sum() print("sub-generator co_sum() done.") idx += 1 def main(): data_sets = { "set_1": [233, 666, 123], "set_2": [1, 2, 4, 5], "set_3": [999, 999, 999], } for key, data_set in data_sets.items(): print("start key:", key) m = middle(key) next(m) # 预激middle协程 for value in data_set: m.send(value) # 给协程传递每一组的值 m.send(None) print("final_result:", final_result) if __name__ == '__main__': main()
这段代码作用是,计算三个数值集每一个集合的总和,运行结果如下,我们依次来看每个函数的作用。
$ python yield_from.py start key: set_1 middle idx: co_sum receive: co_sum receive: co_sum receive: co_sum receive: None sub-generator co_sum . middle idx: start key: set_2 middle idx: co_sum receive: co_sum receive: co_sum receive: co_sum receive: co_sum receive: None sub-generator co_sum . middle idx: start key: set_3 middle idx: co_sum receive: co_sum receive: co_sum receive: co_sum receive: None sub-generator co_sum . middle idx: final_result: : 1022, 233, 666, 123, : 12, 1, 2, 4, 5, : 2997, 999, 999, 999
-
main()函数通过循环分别读取每一个集合的key和一个存有多个数字的list。然后生成一个middle()协程并预激,将列表中的每一个数字通过send方法发过去。发送完毕后调用send(None)结束协程的运行。
-
middle(key)函数其实是一个委托生成器,因为它含有一个yield from <iterator>结构。这个循环每次遍历的时候,会生成一个新的co_sum()实例,每个实例都是一个生成器,当然是作为协程使用的生成器对象。委托生成器会在yield from的时候暂停执行,将控制权交给co_sum(),并等待co_sum()执行结束。
现在再回过头来看这段代码,就比较容易理解了,这里只有一个子生成器和一个委托生成器,如果我们将子生成器换成一个委托生成器,再通过yield from调用其他的子生成器,就可以将任意多个生成器链接起来,只要最终的子生成器通过yield表达式结束即可。
0x02 何为yield from
是时候来深入理解一下yield from这个结构了,根据PEP 380来看,这东西相当难理解,而且及其复杂。下面我们就来试着解释一下RESULT = yield from EXPR。我们先把问题简化一下,做出以下几个假设。
在这些假设下,RESULT = yield from EXPR可以简化成下面这样,在读这段代码之前,我们先说明一下这些下划线开头的变量的作用:
这段代码的大部分含义已经在注释中说明了,可以结合注释来理解这段代码的含义。
_i = iter(EXPR) # EXPR是一个可迭代对象,_i其实是子生成器; try: _y = next(_i) # 预激子生成器,把产出的第一个值存在_y中; except StopIteration as _e: _r = _e.value # 如果抛出了`StopIteration`异常,那么就将异常对象的`value`属性保存到_r,这是最简单的情况的返回值; else: while 1: # 尝试执行这个循环,委托生成器会阻塞; _s = yield _y # 生产子生成器的值,等待调用方`send()`值,发送过来的值将保存在_s中; try: _y = _i.send(_s) # 转发_s,并且尝试向下执行; except StopIteration as _e: _r = _e.value # 如果子生成器抛出异常,那么就获取异常对象的`value`属性保存到_r,退出循环,恢复委托生成器的运行; break RESULT = _r # _r就是整个yield from表达式返回的值。
如果你已经理解了,那么我们继续。现实情况下,yield from要做的事情会复杂一些,毕竟我们做出了一系列的假设。将假设去掉没有什么难度,但是有几个问题非常难受:
这些问题都是yield from结构需要考虑的事情,下面我们来看一下完成的简化之前的代码,相关的注解也写到了注释中:
_i = iter(EXPR) # EXPR是一个可迭代对象,_i其实是子生成器; try: _y = next(_i) # 预激子生成器,把产出的第一个值存在_y中; except StopIteration as _e: _r = _e.value # 如果抛出了`StopIteration`异常,那么就将异常对象的`value`属性保存到_r,这是最简单的情况的返回值; else: while 1: # 尝试执行这个循环,委托生成器会阻塞; try: _s = yield _y # 生产子生成器的值,等待调用方`send()`值,发送过来的值将保存在_s中; except GeneratorExit as _e: try: # 这部分的作用是关闭子生成器和委托生成器,因为子生成器可能是个可迭代对象,所以要处理没有close()方法的情况; _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: # 这部分是处理通过.throw()方法传入的异常,如果子生成器是个迭代器,没有throw()方法的情况下,委托生成器会抛出一个异常 _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: # 子生成器有throw()的情况,那么就调用throw()方法,并且传入调用发提供的异常。这时,子生成器可能会处理异常并继续执行,叶可能会抛出`StopIteration`异常结束执行,也有可能没有处理并且向上"冒泡",抛出异常; try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: # 这部分是当子生成器在执行时没有出现异常的情况 try: if _s is None: # 如果调用方发送的是None,那么就在子生成器上调用next()函数; _y = next(_i) else: # 调用发发送的不是None,调用子生成器的.send()方法; _y = _i.send(_s) except StopIteration as _e: # 如果出现了StopIteration异常,那么就获取异常对象中value属性的值,并保存在返回值中,中断循环,让委托生成器继续运行 _r = _e.value break RESULT = _r # _r就是整个yield from表达式返回的值。