python中和生成器协程相关的yield from之最详最强解释,一看就懂(三)
接上篇, 本节内容主要讲yield from的真正内在含义, yield from相关语法是Python3.3以后引入的, python官宣的解释是这样的
RPE380增加了yield from表达式,
允许一个作为委托方的generator将自己部分操作委托给另一个generator(叫作sub-generator)。
这样就可以让一段包含yield的代码被分散并安置到其它的sub-generator中。
除此之外,sub-generator还可以返回一个值,并且这个值可以方便地被作为委托方的generator所获取
这段话怎么理解?
yield from 提供了一种在caller
和generator/subroutine之间的透明的双向通道从
,即caller可以发送数据到底层corutine,也可以直接从底层的
generator
读取数据
依然不好理解。我考虑了好几种方案,参阅了好多文章,都觉得说不清楚,所以我这里通过一个非常简单模拟系统来一步步详解说明。
一. 系统模型。
假设我们有一个系统, 它的业务需求是这样:
1. 需要读取一段放在一个常量列表中的文本, 每个item表示一行文本。
2. 每读入一行,则先打印双小于号 "<<",然后打印读入的文本行
3. 如果文本全部成功读取,则最后打印“done”表示应用正常结束
二。 系统最初版本,由一个reader和app实现,转化的软件需求分别如下
1. reader是一个generator。
1)用来模拟文本读取,每次读取text中一行,并返回该行文本
2)如果文本全部读完,返回‘done’
2. app是主线业务, 软件需求如下:
1)while循环中,每次通过next读取reader一个生成的值,并在文本前加“<<"后打印
2)如果reader产生StopIteration异常, 则打印reader的返回值
这个初版用python实现如下:
# reader是一个生成器, 每次调用,它将读取列表中一个值并返回 def reader(text): for line in text: yield line return 'done' # app是定义的一个简单应用,将reader读出的值打印出来 def app(text): try: r = reader(text) while True: line = next(r) print('<< %s' % line) except StopIteration as e: print(e.value) #启动app应用 app(('a','b','c','d'))
执行 app(('a','b','c','d')) 得到如下结果
<< a << b << c << d done
三。新需求引入
现在系统需求改变了,在文件开始读取和结束读取之后,需要记录日志以满足运维需求,但系统原有业务无关,是和主线业务正交的非业务需求。为了避免以后再次修改app应用,引入一个代理proxyReader处理这些切面类需求。
1)Reader只涉及底层文本读取,和之前一样。
2)定义前后两个方法。 运维需求省略具体实现,并且抽象成一个before和after方法,里面可以添加记录日志等运维相关的需求
def before(): # 文件开始处理之前的额外处理,比如记录日志 pass def after(): # 文件读取完成之后的额外处理,比如记录日志 pass
3) app的改动:通过代理从reader获取值, 不再直接使用reader,这样所有和app无关的运维相关需求都可以在代理中实现, 而且app和reader均无再做任何修改,所有运维需求带来的改动以后都被将封装在代理中
def app(text): try: r = proxyReader(text) while True: line = next(r) print('<< %s' % line) except StopIteration as e: print(e.value)
4)代理生成器的软件需求。
3-1)不能对reader的产生的值做任何修改,原样上报给app
3-2) 需要处理新加的非主线的运维需求
def proxyReader(text): before() #开始读之前运维需求处理 r = reader(text) while True: line = next(r) yield '<< %s' % line after() #结束读之后运维需求处理
5)完整代码如下
def before(): # 文件开始处理之前的额外处理 pass def after(): # 文件读取完成之后的额外处理 pass # reader是一个生成器, 每次调用,它将读取列表中一个值并返回 def reader(text): for line in text: if line=='': raise EmptyException() else: yield line return 'done' def proxyReader(text): before() #开始读之前运维需求处理 r = reader(text) while True: line = next(r) yield '<< %s' % line after() #结束读之后运维需求处理 def app(text): try: r = proxyReader(text) while True: line = next(r) print('<< %s' % line) except StopIteration as e: print(e.value) #启动app应用 app(('a','b','c','d'))
执行 app(('a','b','c','d')) 得到如下类似结果
<< << a << << b << << c << << d Traceback (most recent call last): File "yieldproxy.py", line 20, in proxyReader line = next(r) StopIteration: done The above exception was the direct cause of the following exception: Traceback (most recent call last): File "yieldproxy.py", line 35, in <module> app(('a','b','c','d')) File "yieldproxy.py", line 28, in app line = next(r) RuntimeError: generator raised StopIteration PS E:\github\python>
四。 问题的引入
结果不是我们所预期的, 出了两个问题:
1. 每行文本都多打印了两个小于符号"<< "
2. ‘done’虽然打印出来了,但是仍然产生一个StopIteration异常
下面我们来分析这两个错误的原因,并解决。
第一个错误,是我们的在编写proxyReader的时候,误读了需求,把主线业务需求(在每行前面新加”<< "当成了proxyReader的需求,结果导致重复打印)
第二个错误,是由于proxyReader在使用reader的时候没有处理StopIteration, 而app处理的是proxyReader的StopIteration. 所以导致输出结果有异常日志
这两个错误的根本原因其实是一个,为了将包含yield的业务代码从app中分离出去, proxyReader采用的是用yield来转换subgenerator的数据到app,由于这个转换操作必须由proxyReader自己实现,所以它实际割裂了真正的reader和caller(也就是app)之间的数据上传通道。下面是基于yield的修改版本,正确实现数据转换和传送
def proxyReader(text): before() #开始读之前运维需求处理 r = reader(text) try: while True: line = next(r) yield line # proxyReader不处理app的逻辑,只负责数据传递 except StopIteration as e: return e.value # 需要处理reader产生的StopIteration, 并把reader的返回值原样返回 after() #结束读之后运维需求处理
再次执行, 结果正确。
五。python从语言级别的解决方案 -- yield from
第四节中的错误,根本原因是proxyReader割裂了reader与app的联系,靠程序员人工保证很不靠谱, 所以这是python3.3引入yield from的真正动力所在, yield from的解决方案如下:
def proxyReader(text): before() #开始读之前运维需求处理 yield from reader(text) after() #结束读之后运维需求处理
这是yield from真正牛逼的地方, 它使proxyReader无需在关心reader与app之间数据通道,这个数据通道被yield from完全封装,对proxyReader透明,而且proxyReader完全无权干涉, 也不可能在有马大哈式的重复打印, reader的输出是啥, yield from百分百保证了app收到的就是啥。除了代码简洁易懂,而且数据安全,牛逼不牛逼 !
以上讲的是,yieldfrom如何搞定从底层的generator到顶层的caller(即app)的数据通道, 那么反过来,如果app要想底层的generator发送数据,是否也能安全透传喃。我们下回分解