python协程系列(三)——yield from原理详解
参考:https://blog.csdn.net/qq_27825451/article/details/85244237
一,yield from的简单实现
从前面系列文章中,我们了解到,yiedl每次“惰性返回”一个值,其实从名字中就能看出,yield from是yield的升级改进版本,如果将yield理解成“返回”,那么yield from就是从“从什么(生成器)里面返回”,这就构成了yield from的一般语法,即
1 | yield from generator |
这样的形式。我们通过一个简单的例子来看
use_yield_from.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # 生成器依次生成0 1 2 3 4 5 6 7 8 9 def generator1(): for i in range ( 10 ): yield i # 演示yield from的用法 def generator2(): yield 'a' yield 'b' yield 'c' yield from generator1() # for item in generator1(): # yield item yield from [ 11 , 22 , 33 , 44 ] yield from ( 11 , 23 , 34 ) yield from range ( 3 ) for i in generator2(): print (i, end = ' , ' ) # a , b , c , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 11 , 22 , 33 , 44 , 11 , 23 , 34 , 0 , 1 , 2 , |
总结
从上面代码输出可以看出,yield from后面可以是"生成器,元组,列表,range()函数产生的序列等可迭代对象"
简单地说,yield from generator。实际上就是返回另外一个生成器。而yield只返回一个元素(即假如yield后面是一个list则返回这个list而不是list里面的元素)。从这个层面来说,有下面的等价关系
1 2 3 4 | yield from iterable # 等价于 for item in Iterable: yield item |
下面例子演示yield和yield from返回的不同
1 2 3 4 5 6 7 | def g(): yield [ 1 , 2 , 3 ] yield from [ 1 , 2 , 3 ] g = g() for item in g: print (item) |
输出如下
1 2 3 4 | [ 1 , 2 , 3 ] 1 2 3 |
解析:使用yield则把整个list作为一个元素返回了,使用yield则把list作为一个可迭代对象把内部的对象一一返回
二,yiedl from的高级应用
倒入,yield from既然称之为yield的升级改进版,如果仅仅是上面的哪一点作用,显然是不够的,因为那仅仅简化了一两句代码的事情。系列文章的上一篇讲解了yield所存在的缺点,参见上一篇文章:https://www.cnblogs.com/minseo/p/15396480.html
yield from正是针对那些不足加以改进的。
1,针对yield无法获取生成器return的返回值
我们都知道,在使用yield生成器的时候,如果使用for语句去迭代生成器,则不会显式的出发StopIteration异常,而是自动捕获StopIteration异常,所以如果遇到return,只是会终止迭代,而不会触发异常,故而也就没办法获取return的值。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # # 使用for循环遇到生成器StopIteration异常不会抛出异常只会终止迭代,没有办法获得return值 def my_generator(): for i in range ( 5 ): if i = = 2 : return '我被迫中断了' else : yield i def main(generator): try : #不会显式触发异常,故而无法获取到return的值 for i in generator: print (i) except StopIteration as exc: print (exc.value) # 调用 g = my_generator() main(g) # 0 # 1 |
说明:因为使用for迭代遇到StopIteration异常也不会抛出异常,而是终止迭代,所以except永远无法捕获到StopIteration异常,所以也无法取得生成器的return值
从上面的例子可以看出,for迭代语句不会显式触发异常,故而无法获取到return的值,迭代到2的时候遇到return语句,隐式的触发了StopIteration异常,就终止迭代了,但是在程序中不会显示出来。
但是如果我是使用next(g)一次一次迭代,则会显式触发异常,但要获取return的返回值,我需要如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # 使用next方法才可以捕获到StopIteration异常从而获得return值 def my_generator(): for i in range ( 5 ): if i = = 2 : return '我被迫中断了' else : yield i def main(generator): try : # 使用next方法每次迭代一个值,则会显式触发StopIteration print ( next (generator)) print ( next (generator)) # 迭代两次以后i值为2则遇到return生成器抛出StopIteration异常 print ( next (generator)) # 因为生成器return了触发了StopIteration异常,所以以下语句不执行 print ( next (generator)) print ( next (generator)) except StopIteration as exc: # 获取返回的值 print (exc.value) g = my_generator() main(g) # 0 # 1 # 我被迫中断了 |
执行两次迭代以后i的值为2,所以生成器return抛出StopIteration异常,异常被捕获以后可以通过异常的value属性获得生成器的return值
现在我们使用yield from来完成上面的同样的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # 使用yield from获得return值 def my_generator(): for i in range ( 5 ): if i = = 2 : return '我被迫中断了' else : yield i # 定义一个包装“生成器”,它的本质还是生成器 def wrap_my_grnerator(generator): # 自动触发StopIteration异常,并且将return的返回值赋值给yield from表达式的结果,即热塑了 result = yield from generator print (result) # 调用主函数,调用的生成器是包装生成器而不是原始生成器 def main(generator): for i in generator: print (i) g = my_generator() wrap_g = wrap_my_grnerator(g) # 调用 main(wrap_g) |
运行结果如下
1 2 3 | 0 1 我被迫中断了 |
解析:
main为主调用函数,调用的生成器为包装生成器wrap_my_generator该包装生成器内部调用的是原始生成器my_generstor,使用yield from可以获得生成器的return值。
从上面的比较可以看出,yield from具有以下几个特点
(1) 上面的my_grnerator是原始生成器,main是调用方,使用yield的时候,值涉及到这两个函数,即“调用方”于“生成器(协程函数)”是直接进行交互的,不涉及其他方法,即“调用方------->生成器(协程函数)”;
(2) 在使用yield from的时候,多了一个队原始my_generstor的包装函数,然后调研方通过这个包装函数(后面会讲到它的专有名词)来于生成器进行交互,即“调研方------>生成器包装函数------>生成器(协程函数)”
(3) yield from iteration结构会在内部自动捕获iteration生成器的StopIteration异常。这种处理方式与for循环处理StopIteration异常的方式一样。而且对yield from结构来说,解释器不仅会捕获StopIteration异常,还会把return返回值或者是StopIteration的value属性变成yield from表达式的值,即上面的result。
2,yiedl from实现的数据传输通道
前面总结的几个特点里面已经介绍了yield和yield from的数据交互方式,yield涉及到“调用方与生成器两者”的交互,生成器通过next()的调用将值返回给调用者,而调用者通过send()方法向生成器发送数据;
但是yield还有一个第三者函数,下面将先从相关的概念说起。
在PEP 380 使用了一些yield from使用的专门术语:
委派生成器:包含 yield from <iterable> 表达式的生成器函数;即上面的wrap_my_generator生成器函数
子生成器:从 yield from 表达式中 <iterable> 部分获取的生成器;即上面的my_generator生成器函数
调用方:调用委派生成器的客户端代码;即上面的main生成器函数
下图是这三者之间的交互关系(摘自博客园):
委派生成器在yield from表达式出暂停时,调用可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,此时委派生成器会恢复。
总结
(1)yield from主要设计用来向子生成器委派操作任务,但yield from可以向任意的可迭代对象委派操作;
(2)委派生成器(group)相当于管道,所以可以把任意数量的委派生成器连接在一起。一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个生成器。
3,针对yield存在的第二个缺点
首先看一下他要表述的意思是什么?它的局限性在于只能向它的直接调用者每次yield一个值。这意味着那些包含yield的代码不能像其他代码那样被分离出来放到一个单独的函数中。这也正是yield from要解决的。具体参见上文:
https://www.cnblogs.com/minseo/p/15396480.html
这句话确实难以理解,但是他要表达的意思实际上是:因为生成器从定义上来看,就像是一个普通的函数,那么既然作为普通函数,就应该可以反反复复调用都没问题的,但是生成器却并不行。那为什么yield from可以解决这样的问题呢,主要是因为yield from后面可以跟任意一个生成器,即yield from可以将任意的任务为派给任意生成器函数,从而避免了子生成器直接向调用者返回单个值的情况
三,yiedl from用法示例
其实yield from最重要的作用就是提供了一个“数据传输管道”,下面通过一个简单的例子加以说明为什么是管道:
例子说明yield from的管道功能.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | def average(): # 数字的总和 total = 0.0 # 数字的个数 count = 0 # 数字的平均值 avg = None while True : # 平均值作为生成器的返回 n = yield avg # 没跌倒一次生成器数字传递的数字个数+1 count = count + 1 # 计算传递数字的和 total = total + n # 计算平均值 avg = total / count def wrap_average(generator): yield from generator def main(wrap): # 启动生成器 print ( next (wrap)) print (warp.send( 10 )) print (warp.send( 20 )) print (warp.send( 30 )) print (warp.send( 40 )) g = average() warp = wrap_average(g) main(warp) # None # 10.0 # 15.0 # 20.0 # 25.0 |
从上面我们可以发现,调研费发送数据是发给wrap_average的,怎么依然到了生成器函数avarage里面呢?这就是“数据传输管道的作用”。即主函数调用方main把各个value传给grouper,而这个传入的值最终达到average函数中;grouper并不知道传入是是什么值,因为从上面的代码看出,wrap_average里面完全没有处理这个值的任何代码 !
现在是不是对“数据传输管道”更加理解了呢?
使用调试模式更加深入理解委派生成器的管道功能
省略几步重复步骤...
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
2020-10-13 Python流程控制
2018-10-13 Redhat7.5安装glusterfs4