优秀程序员Python培训分享深入理解yieldfrom语法,当您阅读本文时,请确保您对生成器知识有一定的了解。你当然看不懂,也没关系,你只要花几分钟看完我上一篇文章,你就能熟悉生成器,上手协程。温馨提示:本系列所有代码均在Python3下编写,建议您尽快投入Python3的怀抱。本文内容?为什么要使用协程?yieldfrom的详细用法?为什么要使用yieldfrom。为什么要使用协程在上一篇文章中,我们成功地从对生成器的基本理解和使用过渡到了协程。但是肯定有很多人只知道什么是协程,却不知道为什么要用协程?也就是说,不知道什么情况下要用协程?与多线程相比,它的优势是什么?在开始讲yieldfrom之前,我想先解决这个让很多人困惑的问题。例如。假设我们做一个爬行动物。我们要抓取多个网页。这里有一个简单的例子,两个网页(两个spider函数),获取HTML(消耗IO和时间),然后解析HTML得到我们感兴趣的数据。我们的代码结构简化如下:defspider_01(url):html=get_html(url)...data=parse_html(html)defspider_02(url):html=get_html(url)...data=parse_html(html)我们都知道get_html()会消耗很多等待返回网页时的IO。一个网页就好了。如果我们抓取的网页数据非常大,那么等待的时间是惊人的,也是一种极大的浪费。聪明的程序员当然会想,如果能在get_html()处暂停一下,就不用傻等网页返回了,而是去做其他的事情。过一会,回到刚才暂停的地方,接收返回的html内容,然后解析parse_html(html)。使用常规方法,几乎??不可能达到我们上面想要的效果。所以Python很周到,从语言本身就为我们实现了这样一个功能,就是yield语法。可以实现函数中暂停的效果。试想一下,如果没有协程,我们还得写一个并发程序。可能存在以下问题1)使用最常规的同步编程来实现异步并发并不理想,或者难度极大。2)由于GIL锁的存在,多线程操作需要频繁加锁、解锁、线程切换,大大降低并发性能;而协程的出现,正好可以解决上述问题。它的特点是。协程在单线程中实现任务切换。它使用同步来实现异步。它不再需要锁并提高并发性能。yieldfrom的详细用法Yieldfrom只在Python3.3语法中出现过。所以这个特性在Python2中是没有的。yieldfrom后面需要添加的是可迭代对象,可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。简单应用:拼接可迭代对象我们可以比较一个使用yield的例子和一个使用yieldfrom的例子。使用yieldstringastr='ABC'#listalist=[1,2,3]#dictionaryadict={"name":"wangbm","age":18}#generatoragen=(iforiinrange(4,8))defgen(args,*kw):foriteminargs:foriinitem:yieldinnew_list=gen(astr,alist,adict,agen)print(list(new_list))#['A','B','C',1,2,3,'name','age',4,5,6,7]使用yieldfromstringastr='ABC'#listalist=[1,2,3]#dictionaryadict={"name":"wangbm","age":18}#generatoragen=(iforiinrange(4,8))defgen(args,*kw):foriteminargs:yieldfromitemnew_list=gen(astr,alist,adict,agen)print(list(new_list))#['A','B','C',1,2,3,'name','age',4,5,6,7]从上面两种方法的对比可以看出,如果yieldfrom后面跟着一个可迭代对象,就可以把可迭代对象中的每一个元素一个一个地yield出来,比yield的代码更简洁而且结构更清晰。复杂应用:生成器的嵌套如果你认为yieldfrom只有上面提到的功能,那你就小看它了,它更强大的功能还在后头。当在yieldfrom后面加上一个generator,就实现了generated的嵌套。当然,要实现生成器的嵌套,不一定要用yieldfrom,但是用yieldfrom可以让我们避免让我们自己处理各种意外的异常,让我们专注于业务代码的实现。如果自己使用yield来实现,只会增加代码编写的难度,降低开发效率,降低代码的可读性。既然Python这么有心,我们当然要好好利用它。在讲解之前,你必须先知道这些概念1.Caller:调用委托生成器的客户端(caller)代码2.委托生成器:包含表达式的yieldfrom的生成器函数3.子生成器:yield你可能不知道在from之后添加的生成器函数是什么意思。没关系。让我们来看看这个例子。本例是实现平均值的实时计算。比如第一次传入10,返回的平均值自然是10,第二次传入20,返回的平均值是(10+20)/2=15,第二次传入30第三次,平均值返回(10+20+30)/3=20subgeneratordefaverage_gen():total=0count=0average=0whileTrue:new_num=yieldaveragecount+=1total+=new_numaverage=total/count委托生成器defproxy_gen():whileTrue:yieldfromaverage_gen()callerdefmain():calc_average=proxy_gen()next(calc_average)#预激生成器print(calc_average.send(10))#print:10.0print(calc_average.send(20))#Print:15.0print(calc_average.send(30))#Print:20.0ifname=='__main__':main()仔细阅读上面的代码,你应该很容易理解,调用者、委托生成器、子生成器之间的关系。我不会说太多。委托生成器的作用是在调用者和子生成器之间建立双向通道。所谓的双向通道是什么意思?调用者可以直接通过send()向子生成器发送消息,子生成器yield的值也直接返回给调用者。你可能经常看到一些代码可以在yieldfrom前面赋值。这是什么用法?你可能认为子生成器yield返回的值被委托生成器拦截了。你可以自己写个demo跑起来,??不是你想的那样。因为我们之前说过,delegationgenerator只是起到一个桥梁的作用,它建立了一个双向通道。它没有权利也没有办法拦截子生成器yield返回的内容。为了解释这个用法,我还是沿用上面的例子,稍微修改一下。添加了一些注意事项,希望您能理解。照例,我们还是举个例子。子生成器defaverage_gen():total=0count=0average=0whileTrue:new_num=yieldaverageifnew_numisNone:breakcount+=1total+=new_numaverage=total/count#每次返回表示当前协议程序结束.returntotal,count,averagedelegategeneratordefproxy_gen():whileTrue:#只有当子生成器即将结束(返回)时,才会对yieldfrom左边的变量赋值,执行下面的代码.total,count,average=yieldfromaverage_gen()print("完成!!\n一共传入了{}个值,sum:{},average:{}".format(count,total,average))callFangdefmain():calc_average=proxy_gen()next(calc_average)#预激协程print(calc_average.send(10))#打印:10.0print(calc_average.send(20))#打印:15.0print(calc_average.send(30))#Print:20.0calc_average.send(None)#Endcoroutine#如果这里又调用了calc_average.send(10),由于之前的协程已经结束,如果name=='__main__会重新开启一个协程':main()运行后,计算出输出10.015.020.0!!一共传入3个值,总和:60,平均值:20.0。为什么要使用yieldfrom?学习到这里,相信你一定会问,既然delegategenerator只是作为一个双向通道,那我还需要delegatebuilder做什么呢?我的调用者直接调用子生成器不是很好吗?高能预警~~~一起来探讨一下,yieldfrom到底有什么特别之处,让我们一起来使用吧。因为去掉delegategenerator直接调用childgenerator可以帮助我们处理异常。那么我们需要将代码改成如下,我们需要捕获异常并自己处理。而不是担心yieldfrom。subgenerator#subgeneratordefaverage_gen():total=0count=0average=0whileTrue:new_num=yieldaverage如果new_numisNone:breakcount+=1total+=new_numaverage=total/countreturntotal,count,averagecallFangdefmain():calc_average=average_gen()next(calc_average)#预激协程print(calc_average.send(10))#打印:10.0print(calc_average.send(20))#打印:15.0print(calc_average.send(30)))#Print:20.0#--------------注意--------------try:calc_average.send(None)exceptStopIterationase:total,count,average=e.valueprint("完成!!\n一共传入了{}个值,sum:{},average:{}".format(count,total,average))#--------------注意----------------ifname=='__main__':main()在此时间,你可能会说,不就是一个StopIteration异常吗?自己抓也没什么大不了的。如果您知道yieldfrom在幕后为我们做了什么,您就不会那样说了。yieldfrom具体为我们做了什么,可以参考下面的代码。一些指令"""_i:子生成器,也是一个迭代器_y:子生成器产生的值_r:yieldfromexpression的最终值_s:调用者通过send()发送的值_e:Exceptionobject"""_i=iter(EXPR)try:_y=next(_i)exceptStopIterationas_e:_r=_e.valueelse:while1:try:_s=yield_yexceptGeneratorExitas_e:try:_m=_i.关闭,除了AttributeError:传递else:_m()raise_e除了BaseExceptionas_e:_x=sys.exc_info()try:_m=_i.throwexceptAttributeError:raise_eelse:try:_y=_m(*_x)exceptStopIterationas_e:_r=_e.valuebreakelse:try:if_sisNone:_y=next(_i)else:_y=_i.send(_s)exceptStopIterationas_e:_r=_e.valuebreakRESULT=_r上面的代码,它是有点复杂,有兴趣的同学可以结合下面的说明学习。.迭代器(即子生成器)产生的值直接返回给调用者。使用send()方法发送给委托生产者(即外部生产者)的任何值都直接传递给迭代器。如果发送值为None,则调用迭代器next()方法;如果它不是None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,则委托生产者恢复并继续执行yieldfrom之后的语句;如果迭代器产生任何其他异常,它会被传递给委托生产者。.sub-generator可能只是一个迭代器,而不是作为协程的生成器,所以它不支持.throw()和.close()方法,即它可能会产生AttributeError异常。.除了GeneratorExit异常外,抛给委托生成器的异常将被传递给迭代器的throw()方法。如果迭代器throw()调用引发StopIteration异常,则委托生产者恢复并继续执行,并将其他异常传递给委托生产者。.如果向委托生产者抛出GeneratorExit异常,或者委托生产者的close()方法被调用,如果迭代器有close()也会被调用。如果close()调用产生异常,则该异常将传递给委托生产者。否则,委托生产者将抛出GeneratorExit异常。.当迭代器结束并抛出异常时,yieldfrom表达式的值是其StopIteration异常中的第一个参数。.生成器中的returnexpr语句将退出生成器并引发StopIteration(expr)异常。没兴趣看的同学,只知道yieldfrom为我们做了很多异常处理,而且很全面。如果我们要自己实现这些,一是写代码的难度增加,写出来的代码可读性极差。这些我们先不说,最重要的是可能会有遗漏,只要不考虑任何异常,就可能导致程序崩溃什么的。
