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

PythonYieldGenerator详解

时间:2023-03-17 16:59:10 科技观察

本文将从浅到深详细介绍yield和generator,包括以下内容:什么是generator,生成generator的方法,generator的特点,基础和进阶应用发电机的使用场景,以及使用发电机时的注意事项。本文不包括增强生成器,即pep342相关内容。生成器的基础是python的函数定义。只要出现一个yield表达式(Yieldexpression),那么实际上就是定义了一个生成器函数,调用这个生成器函数的返回值就是一个生成器。这个普通的函数调用是不同的,例如:defgen_generator():yield1defgen_value():return1if__name__=='__main__':ret=gen_generator()printret,type(ret)#ret=gen_value()printret,type(ret)#1从上面的代码可以看出,gen_generator函数返回一个生成器实例,而这个生成器有以下特殊的特性:遵循迭代器(iterator)协议,iterator协议需要实现__iter__,next接口可以多次进入和返回,可以暂停函数体中代码的执行。我们看一下测试代码:>>>defgen_example():...print'beforeanyyield'...yield'firstield'...print'betweenyields'...yield'secondyield'...print'noyieldanymore'...>>>gen=gen_example()>>>gen.next()    #***secondcallnextbeforeanyield'firstield'>>>gen.next()    #secondcallnextbetweenyields'secondyield'>>>gen.next()    #thirdcallnextnoyieldanymoreTraceback(mostrecentcalllast):File"",line1,inStopIteratio调用了gen示例方法,没有输出任何东西,说明代码函数体还没有开始执行。当调用generator的next方法时,generator会执行到yield表达式,返回yield表达式的内容,然后在这个地方暂停(suspend),所以第一次调用next打印语句,返回"first屈服”。挂起是指方法的局部变量、指针信息、运行环境都被保存,直到下次调用下一个方法恢复。第二次调用next后,会在最后一次yield处暂停,再次调用next()方法会抛出StopIteration异常。因为for语句可以自动捕获StopIteration异常,generator(本质上是任何迭代器)更常用的方法是在循环中使用它:generator和普通函数有什么区别?Function每次都是从***行开始运行,而generator是从上次yield开始的地方运行。函数调用一次返回一个(一组)值,生成器可以多次返回函数可以调用无数次,一个生成器实例在yield***一个值或return后不能继续调用.在函数中使用Yield,然后调用函数是生??成生成器的一种方式。另一种常见的方式是使用生成器表达式,例如:>>>gen=(x*xforxinxrange(5))>>>printgenat0x02655710>generatorapplicationgeneratorbasicapplication为什么要使用generator,最重要的原因就是可以按需生成结果并“返回”,而不是一下子生成所有的返回值,而且有时候“所有的返回值”根本不知道。例如,对于下面的代码:RANGE_NUM=100foriin[x*xforxinrange(RANGE_NUM)]:#***方法:迭代列表#dosthforexampleprintiforiin(x*xforxinrange(RANGE_NUM)):#第二种方法:迭代生成器#dosthforexampleprinti在上面的代码中,两个for语句的输出是一样的,代码从字面上看起来就是方括号和圆括号的区别。但是这个区别是很不一样的。第一个方法的返回值是一个列表,第二个方法的返回值是一个生成器对象。随着RANGE_NUM变大,第一种方法返回更大的列表,占用更多内存;但是第二种方法没有区别。我们来看一个可以无限次“返回”的例子:deffib():a,b=1,1whileTrue:yieldaa,b=b,a+b这个生成器有生成无数次“返回值”的能力,用户你可以决定何时停止迭代。Generator进阶应用场景一:Generator可以用来生成数据流。生成器不会立即生成返回值,而是等到需要的时候再生成返回值,相当于一个主动的拉取过程(pull)。例如,有一个日志文件每行生成一条记录。不同部门的人可能会以不同的方式处理每条记录,但我们可以提供一个通用的、按需生成的数据流。defgen_data_from_file(文件名):forlineinfile(文件名):yieldlinedefgen_words(行):forwordin(wforwinline.split()ifw.strip()):yieldworddefcount_words(文件名):word_map={}forlineingen_data_from_file(文件名):forwordingen_words(行):ifwordnotinword_map:word_map[word]=0word_map[word]+=1返回word_mapdefcount_total_chars(file_name):total=0forlineingen_data_from_file(file_name):total+=len(line)returntotalif__name__=='__main__':printcount_words('test.txt'),count_total_chars('test.txt')上面的例子来自2008年PyCon的一个讲座,gen_wordsgen_data_from_file是数据生产者,count_wordscount_total_chars是数据消费者。可以看出数据只是在需要的时候拉取,而不是提前准备好。另外gen_words中的(wforwinline.split()ifw.strip())也生成了一个生成器。场景二:在一些编程场景中,一个事物可能需要执行一部分逻辑,然后等待一段时间,或者等待一个异步结果,或者等待某个状态,然后继续执行另一部分逻辑。比如在微服务架构中,服务A执行完一段逻辑后,向服务B请求一些数据,然后在服务A上继续执行。或者在游戏编程中,一个技能被分成多个段,先执行一个部分动作(效果),然后等待一段时间再继续。对于这种需要等待又不想阻塞的情况,我们一般使用回调的方式。这是一个简单的例子:defdo(a):print'do',aCallBackMgr.callback(5,lambdaa=a:post_do(a))defpost_do(a):print'post_do',awhereCallBackMgrregistereda5sAfter5seconds,再次调用lambda函数。可以看到,一段逻辑拆分成了两个函数,还需要传递上下文(比如这里的参数a)。我们用yield来修改这个例子,yield的返回值代表等待时间。@yield_decdefdo(a):print'do',ayield5print'post_do',a这里需要实现一个YieldManager,通过yield_decdecrator将do生成器注册到YieldManager中,5s后调用next方法。Yield版本实现了与回调相同的功能,但看起来更简洁。下面给出一个简单的现以供参考:#-*-coding:utf-8-*-importsys#importTimerimporttypesimporttimeclassYieldManager(object):def__init__(self,tick_delta=0.01):self.generator_dict={}#self._tick_timer=定时器.addRepeatTimer(tick_delta,lambda:self.tick())deftick(self):cur=time.time()forgene,tinself.generator_dict.items():ifcur>=t:self._do_resume_genetator(gene,cur)def_do_resume_genetator(self,gene,cur):try:self.on_generator_excute(gene,cur)exceptStopIteration,e:self.remove_generator(gene)exceptException,e:print'unexcepeterror',类型(e)self.remove_generator(gene)defadd_generator(self,gen,deadline):self.generator_dict[gen]=deadlinedefremove_generator(self,gene):delself.generator_dict[gene]defon_generator_excute(self,gen,cur_time=None):t=gen.next()cur_time=cur_timeortime.time()self.add_generator(gen,t+cur_time)g_yield_mgr=YieldManager()defyield_dec(func):def_inner_func(*args,**kwargs):gen=func(*args,**kwargs)iftype(gen)istypes.GeneratorType:g_yield_mgr.on_generator_excute(gen)returgenreturn_inner_func@yield_decdefdo(a):print'do',ayield2.5print'post_do',ayield3print'post_doagain',aif__name__=='__main__':do(1)foriinrange(1,10):print'simulatetimer,%ssecondsspassed'%itime.sleep(1)g_yield_mgr.tick()注意事项:(1)yield不能嵌套!defvisit(data):forelemindata:ifisinstance(elem,tuple)orisinstance(elem,list):visit(elem)#herevalueretuenedisgeneratorelse:yiedelemif__name__=='__main__':foreinvisit([1,2,(3,4),5]):printe上面的代码访问了嵌套序列中的每个元素,我们期望的输出是12345,但是实际输出是125为什么,如注释所示,visit是一个生成器函数,所以第4行返回一个生成器对象,代码并没有遍历这个生成器实例。然后更改代码并迭代这个临时生成器。defvisit(data):forelemindata:ifisinstance(elem,tuple)orisinstance(elem,list):foreinvisit(elem):yieldeelse:yieldelem或yieldfrom可以在python3.3中使用,此语法在pep380中添加:defvisit(data):forelemindata:ifisinstance(elem,tuple)orisinstance(elem,list):yieldfromvisit(elem)else:yieldelem(2)生成器函数中使用return在pythondoc中明确提到当生成器时可以使用return执行到这里,抛出了一个StopIteration异常。defgen_with_return(range_num):ifrange_num<0:returnelse:foriinxrange(range_num):yieldiif__name__=='__main__':printlist(gen_with_return(-1))printlist(gen_with_return(1))但是generator函数中的return不能带来任何return值得。defgen_with_return(range_num):ifrange_num<0:return0else:foriinxrange(range_num):yieeldi上面的代码会报错:SyntaxError:'return'withargumentinsidegenerator参考http://www.dabeaz.com/generators-uk/https://www.python.org/dev/peps/pep-0380/http://stackoverflow.com/questions/231767/what-does-the-yield-keyword-dohttp://stackoverflow.com/questions/15809296/python-syntaxerror-return-with-argument-inside-generator