当前位置: 首页 > 科技观察

Python协程你知多少

时间:2023-03-13 19:07:55 科技观察

从概念上讲,我们都知道多进程和多线程,而协程实际上是在单线程中实现了多个并发。从句法上看,协程类似于生成器,是在定义体中包含yield关键字的函数。不同的是协程的yield通常出现在表达式的右边:datum=yield。这让初学者瞬间觉得yield关键字不好用。我以为yield就是简单的暂停执行返回一个值,结果可以放在右边?从生成器到协程,先看一个可能是协程最简单的用法示例:>>>defsimple_coroutine():...print("->coroutinestarted")...x=yield...print("->coroutinereceived:",x)...>>>my_coro=simple_coroutine()>>>my_coro>>>next(my_coro)->coroutinestarted>>>my_coro.send(42)->coroutinereceived:42Traceback(mostrecentcalllast):File"",line1,inStopIterationyield之所以可以放在右边,是因为协程可以接收调用者使用.send()推送的值。yield放在右边之后,可以在它的右边放另一个表达式,请看下面的例子:defsimple_coro2(a):b=yieldac=yielda+bmy_coro2=simple_coro2(14)next(my_coro2)my_coro2.send(28)my_coro2.send(99)的执行过程是:调用next(my_coro2),执行yielda,输出14。调用my_coro2.send(28),将28赋值给b,然后执行yielda+b,输出42。调用my_coro2.send(99),将99赋值给c,协程终止。由此可以得出,对于b=yielda这行代码,=右边的代码是在赋值之前执行的。例子中需要调用next(my_coro)启动生成器,让程序在yield语句处暂停,然后才能发送数据。这是因为协程有四种状态:'GEN_CREATED'等待开始执行'GEN_RUNNING'解释器正在执行'GEN_SUSPENDED'在yield表达式处暂停'GEN_CLOSED'执行结束时,只能在GEN_SUSPENDED状态发送数据,这预先做的叫做预激,可以调用next(my_coro)预激,也可以调用my_coro.send(None)预激,效果是一样的。预激协程协程必须预激后才能使用,即在send之前先调用next,使协程处于GEN_SUSPENDED状态。但是这件事经常被遗忘。为了避免忘记,可以定义一个预激装饰器,如:fromfunctoolsimportwrapsdefcoroutine(func):@wraps(func)defprimer(*args,**kwargs):gen=func(*args,**kwargs)next(gen)returngenreturnprimer但其实Python给出了一种更优雅的方式,叫做yieldfrom,它会自动预激活协程。自定义预烧装饰器与yieldfrom不兼容。yieldfromyieldfrom等同于其他语言中的await关键字。它的作用是:在生成器gen中使用yieldfromsubgen()时,subgen会获得控制权,并将输出值传递给gen的调用者,即调用者可以直接控制subgen。同时,gen阻塞,等待subgen终止。yieldfrom可用于简化for循环中的yield:forcin"AB":yieldcyieldfrom"AB"yieldfromx表达式对x所做的第一件事是调用iter(x)从中获取迭代器。但是yieldfrom的作用远不止于此。它更重要的功能是打开双向通道。如下图所示:这个图信息量很大,也比较难理解。首先我们要了解这三个概念:caller、delegategenerator、sub-generator。说白了,调用者就是main函数,也就是大家熟知的程序入口main函数。#theclientcode,a.k.a.thecallerdefmain(data):#<8>results={}forkey,valuesindata.items():group=grouper(results,key)#<9>next(group)#<10>forvalueinvalues:group.send(value)#<11>group.send(None)#important!<12>#print(results)#uncommenttodebugreport(results)delegationgenerator是一个包含yieldfrom语句的函数,即协程。#thedelegatinggeneratordefgrouper(results,key):#<5>whileTrue:#<6>results[key]=yieldfromaverager()#<7>子生成器是子协程后跟yieldfrom语句。#thesubgeneratordefaverager():#<1>total=0.0count=0average=NonewhileTrue:term=yield#<2>iftermisNone:#<3>breaktotal+=termcount+=1average=total/countreturnResult(count,average)#<4>this比术语舒服多了。然后是5行:send、yield、throw、StopIteration、close。当send协程挂在yieldfrom表达式时,主函数可以通过yieldfrom表达式向yieldfrom语句右侧的子协程发送数据。yieldyieldfrom语句右侧的子协程通过yieldfrom表达式将输出值发送给主函数。throwmain函数通过group.send(None)传入一个None的值,从而终止右边yieldfrom语句后面的子协程的while循环,从而将控制权交还给协程,可以继续执行,否则会暂时保持yieldfrom语句暂停。在StopIterationyieldfrom语句之后的生成器函数返回后,解释器抛出StopIteration异常。并将返回值附在异常对象上,此时协程会恢复。closemain函数执行后,会调用close()方法退出协程。大体流程清楚,更多技术细节不再继续研究。有时间的话可以在以后的Python原理系列中学习。yieldfrom通常与Python3.4标准库中的@asyncio.coroutine装饰器结合使用。协程用作累加器。这是协程的常见用法。代码如下:defaverager():total=0.0count=0average=NonewhileTrue:#<1>term=yieldaverage#<2>total+=termcount+=1average=total/count协程实现并发这里的例子有点复杂,源码地址为:https://github.com/fluentpython/example-code/blob/master/16-coroutine/taxi_sim.py核心代码片段为:#BEGINTAXI_PROCESSdeftaxi_process(ident,trips,start_time=0):#<1>"""Yieldtosimulatorissuingeventateachstatechange"""time=yieldEvent(start_time,ident,'leavegarage')#<2>foriinrange(trips):#<3>time=yieldEvent(time,ident,'pickuppassenger')#<4>time=yieldEvent(time,ident,'dropoffpassenger')#<5>yieldEvent(time,ident,'goinghome')#<6>#endoftaxiprocess#<7>#ENDTAXI_PROCESSdefmain(end_time=DEFAULT_END_TIME,num_taxis=DEFAULT_NUMBER_OF_TAXIS,seed=None):"""Initializerrandomgenerator,buildprocsandrunsimulation"""ifseedisnotNone:random.seed(seed)#getreproducibleresultstaxis={i:taxi_process(i,(i+1)*2,i*DEPARTURE_INTERVAL)foriinrange(num_taxis)}sim=Simulator(taxis)sim.run(end_time)这个例子说明了如何在主循环中处理事件以及如何通过发送数据来驱动协程。这是asyncio包背后的基本思想。使用协程而不是线程和回调来实现并发。参考资料:《流畅的Python》第16章协程https://zhuanlan.zhihu.com/p/104918655