Python 3中的yield from语法详解
前言
最近在捣鼓Autobahn,它有给出个例子是基于asyncio的,想着说放到pypy3上跑跑看竟然就……失败了。pipinstallasyncio直接报invalidsyntax,粗看还以为2to3处理的时候有问题——这不能怪我,好~多package都是用2写了然后转成3的——结果发现asyncio本来就只支持3.3+的版本,才又回头看代码,赫然发现一句yieldfrom;yield我知道,但是yieldfrom是神马?
PEP-380
好吧这个标题是我google出来的,yieldfrom的前世今生都在这个PEP里面,总之大意是原本的yield语句只能将CPU控制权还给直接调用者,当你想要将一个generator或者coroutine里带有yield语句的逻辑重构到另一个generator(原文是subgenerator)里的时候,会非常麻烦,因为外面的generator要负责为里面的generator做消息传递;所以某人有个想法是让python把消息传递封装起来,使其对程序猿透明,于是就有了yieldfrom。
PEP-380规定了yieldfrom的语义,或者说嵌套的generator应该有的行为模式。
假设A函数中有这样一个语句
yieldfromB()
B()返回的是一个可迭代(iterable)的对象b,那么A()会返回一个generator——照我们的命名规范,名字叫a——那么:
- b迭代产生的每个值都直接传递给a的调用者。
- 所有通过send方法发送到a的值都被直接传递给b.如果发送的值是None,则调用b的__next__()方法,否则调用b的send方法。如果对b的方法调用产生StopIteration异常,a会继续执行yieldfrom后面的语句,而其他异常则会传播到a中,导致a在执行yieldfrom的时候抛出异常。
- 如果有除GeneratorExit以外的异常被throw到a中的话,该异常会被直接throw到b中。如果b的throw方法抛出StopIteration,a会继续执行;其他异常则会导致a也抛出异常。
- 如果一个GeneratorExit异常被throw到a中,或者a的close方法被调用了,并且b也有close方法的话,b的close方法也会被调用。如果b的这个方法抛出了异常,则会导致a也抛出异常。反之,如果b成功close掉了,a也会抛出异常,但是是特定的 GeneratorExit异常。
- a中yieldfrom表达式的求值结果是b迭代结束时抛出的 StopIteration异常的第一个参数。
- b中的return<expr>语句实际上会抛出StopIteration(<expr>)异常,所以b中return的值会成为a中yieldfrom表达式的返回值。
为神马会有这么多要求?因为generator这种东西的行为在加入throw方法之后变得非常复杂,特别是几个generator在一起的情况,需要类似进程管理的元语对其进行操作。上面的所有要求都是为了统一generator原本就复杂的行为,自然简单不下来啦。
我承认我一下没看明白PEP的作者到底想说什么,于是动手“重构”一遍大概会有点帮助。
一个没用的例子
说没用是因为你大概不会真的想把程序写成这样,但是……反正能说明问题就够了。
设想有这样一个generator函数:
definner(): coef=1 total=0 whileTrue: try: input_val=yieldtotal total=total+coef*input_val exceptSwitchSign: coef=-(coef) exceptBreakOut: returntotal
这个函数生成的generator将从send方法接收到的值累加到局部变量total中,并且在收到BreakOut异常时停止迭代;至于另外一个SwitchSign异常应该不难理解,这里就不剧透了。
从代码上看,由inner()函数得到的generator通过send接收用于运算的数据,同时通过throw方法接受外部代码的控制以执行不同的代码分支,目前为止都很清晰。
接下来因为需求有变动,我们需要在inner()这段代码的前后分别加入初始化和清理现场的代码。鉴于我认为“没坏的代码就不要动”,我决定让inner()维持现状,然后再写一个outer(),把添加的代码放在outer()里,并提供与inner()一样的操作接口。由于inner()利用了generator的若干特性,所以outer()也必须做到这五件事情:
- outer()必须生成一个generator;
- 在每一步的迭代中,outer()要帮助inner()返回迭代值;
- 在每一步的迭代中,outer()要帮助inner()接收外部发送的数据;
- 在每一步的迭代中,outer()要处理inner()接收和抛出所有异常;
- 在outer()被close的时候,inner()也要被正确地close掉。
根据上面的要求,在只有yield的世界里,outer()可能是长这样的:
defouter1(): print("Beforeinner(),Idothis.") i_gen=inner() input_val=None ret_val=i_gen.send(input_val) whileTrue: try: input_val=yieldret_val ret_val=i_gen.send(input_val) exceptStopIteration: break exceptExceptionaserr: try: ret_val=i_gen.throw(err) exceptStopIteration: break print("Afterinner(),Idothat.")
WTF,这段代码比inner()本身还要长,而且还没处理close操作。
现在我们来试试外星科技:
defouter2(): print("Beforeinner(),Idothis.") yieldfrominner() print("Afterinner(),Idothat.")
除了完全符合上面的要求外,这四行代码打印出来的时候还能省点纸。
我们可以在outer1()和outer2()上分别测试数据以及异常的传递,不难发现这两个generator的行为基本上是一致的。既然如此,外星科技当然在大多数情况下是首选。
对generator和coroutine的疑问
从以前接触到Python下的coroutine就觉得它怪怪的,我能看清它们的行为模式,但是并不明白为什么要使用这种模式,generator和coroutine具有一样的对外接口,是generator造就了coroutine呢,还是coroutine造就了generator?最让我百思不得其解的是,Python下的coroutine将“消息传递”和“调度”这两种操作绑在一个yield上——即便有了yieldfrom,这个状况还是没变过——我看不出这样做的必要性。如果一开始就从语法层面将这两种语义分开,并且为generator和coroutine分别设计一套接口,coroutine的概念大概也会容易理解一些。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家学习或者使用python能带来一定的帮助,如果有疑问大家可以留言交流。