当前位置: 首页 > 后端技术 > Python

一系列巩固一个知识点-python生成器

时间:2023-03-26 15:20:25 Python

写在前面。迭代是数据处理的基石。在扫描无法加载到内存中的数据集时,我们需要具备惰性获取数据的能力(即一次获取一部分数据到内存中)。在Python中,具有此功能的对象是迭代器。生成器是迭代器的一种特殊形式。就个人而言,我认为生成器是Python中最有用的高级功能之一(甚至不是一个)。虽然在初级编码中很少用到,但是随着学习的深入,你会发现生成器是协程、异步等高级知识的基石。Python最雄心勃勃的asyncio库是用协程构建的。注意:生成器和协程本质上是一样的。PEP342(PythonEnhancementProposal)添加了生成器的send()方法,将其变成协程。之后生成器生成数据,协程消费数据。虽然本质是一样的,但是由于coroutine在概念上和迭代没有任何关系,而entanglementgenerator和coroutine的区别和联系会引爆自己的大脑,所以这两个概念还是要分清的。这里同义的本质意思是:理解了generator的原理后,明白了send方法已经加了,但是实现方法几乎一样的协程会更简单(不会也无所谓)看不懂这段话,船会直奔桥头,学习协程程自然就懂了)。Python的一致性是它最迷人的地方。了解Python生成器和迭代器的实现。你会对Python一贯的设计有更强烈的感受。看完这篇,当面试官问到为什么列表可以迭代,字典可以迭代,甚至文本文件可以迭代时,你就能搞定一批。在阅读本文之前,如果您对Python的一致性有一些了解,例如ducktyping或CPython的PyObject结构,那就太好了。不过,鉴于作者深厚的文笔功底,你没有这方面的知识也没关系。Dryiterators在学习生成器之前,你必须先了解迭代器。顾名思义,迭代器是一个能够迭代的对象。在Python中,可以认为迭代器可以通过不断的迭代,生成一个又一个的对象。协议支持Python中可迭代对象和迭代器的一致性。只要对象符合以下协议,它就是可迭代对象或迭代器。Python中的对象如果实现了iter方法并且iter方法返回一个迭代器,那么它就是一个可迭代对象。如果实现了iter和next方法,并且iter方法返回的是一个迭代器,那么就是一个迭代器(有点迷糊,按住不放,继续学习)。注意:如果对象实现了__getitem__方法,索引从0开始,也是可迭代对象。此hack是出于兼容性考虑。请记住,如果您实现可迭代对象和可迭代对象,请遵循上面的协议。可迭代对象的iter返回迭代器,迭代器的iter方法返回自身(也是迭代器),迭代器的next方法实现迭代函数,不断返回下一个元素,或引发StopIteration终止迭代时该元素为空。可迭代对象和迭代器的关系就不多说了,上代码吧。可迭代类:def__init__(self,*args):self.items=argsdef__iter__(self):returnIterator(self.items)迭代器类:def__init__(self,items):self.items=itemsself.index=0def__iter__(self):returnselfdef__next__(self):try:item=self.items[self.index]除了IndexError:raiseStopIteration()self.index+=1returnitems=Iterable(1,2,3,4,5)#1foriinins:print(i)print('theend...')>>>#212345theend...上面代码中实现了可迭代对象Iterable和迭代器Iterator。按照约定,Iterable实现了iter方法,iter方法返回迭代器的Iterator实例,iterator实现了iter方法和next方法,iter返回自己(即sel,迭代器本身f),next方法返回迭代器中的元素或引发StopIteration异常。运行上面的代码,您将在#2看到输出。通过上面的代码迭代一个对象非常冗长。例如,在Iterable中,iter必须返回一个迭代器。为什么不能用Iterator直接迭代元素呢?假设我们通过一个迭代器来迭代元素,上面代码中#1处的代码如下:ins=Iterator([1,2,3,4,5])foriinins:#3print(i)foriinins:#4print(i)next(ins)#5print('theend...')>>>#612345...文件“/home/disk/test/a.py”,第20行,在__next__#7raiseStopIteration()theend...运行上面的代码,您将在#6处看到输出。令人困惑的是,在#3和#4处运行了两个for循环,并且所有元素只打印一次。解释如下:上面代码中,ins是一个Iterator迭代器对象。那么ins符合迭代器协议:每次调用next时,都会返回下一个元素,直到迭代器元素为空,才会抛出StopIteration异常。#3处第一次通过for循环迭代ins,相当于不断调用ins的next方法连续返回下一个元素,输出结果如#6所示。当元素为空时,迭代器引发StopIterator。而这个异常会被for循环捕获,不会暴露给用户,所以我们认为数据迭代完成,没有异常发生。迭代器ins中的元素已被#3处的for循环消耗,并且StopIteration已被引发(仅被for循环捕获并静默处理,不暴露给用户)。此时ins已经处于元素耗尽的“空”状态。在#4,ins第二次通过for循环迭代。因为ins里面的元素都是空的,继续调用ins的next方法,那么还是会抛出一个StopIteration,会被for循环静默处理,所以没有异常,没有输出。接下来通过#5处的next方法获取ins的下一个元素,同上,继续抛出StopIteration异常。由于这里使用了next调用而不是for循环,所以不会处理异常,所以抛给用户层,即#7输出。重写上面代码中#3和#4处的for循环,可以看到对应的输出结果验证了我们的结论。第一个for循环迭代到元素2时跳出循环,第二个for循环继续迭代同一个迭代器,然后继续迭代最后一个迭代器结束位置的元素。代码如下:ins=Iterator([1,2,3,4,5])print('thefirstfor:')foriinins:#3thefirstforprint(i)ifi==2:breakprint('thesecondfor:')foriinins:#4thesecondforprint(i)print('theend...')>>>#theoutputthefirstfor:12thesecondfor:345theend...so我们可以得出如下结论:一个迭代器对象只能迭代一次。多次迭代相当于在空迭代器上不断调用next方法,这将不断引发StopIteration异常。由于迭代器实现了iter方法,而iter方法返回的是迭代器,所以迭代器也是可迭代对象(废话,不是可迭代对象,上面代码中的for循环怎么迭代)综上所述,可迭代对象和迭代器是显然是一个多态问题。迭代器是可以迭代返回元素的可迭代对象。由于iter返回的是self(即自己的实例),因此只能迭代一次,迭代结束会抛出异常。并且每次迭代一个可迭代对象时,iter都会返回一个新的迭代器实例。所以可迭代对象支持多次迭代。比如l=[iforiinrange(10)]生成的list对象就是一个可迭代对象,可以迭代多次。l=(iforiinrange(10))生成一个只能迭代一次的迭代器。迭代器支持在流利的Python中引用原文,迭代器支持以下6个功能。限于篇幅,就到此为止吧。只要理解了迭代器的原理,后面的函数就自然而然的理解了。for循环上面的代码已经有例子了,可以参考buildingandextendingcollectiontypefromcollectionsderivationl=[iforiinrange(10)]#listd={i:iforiinrange(10)}#dicts={iforiinrange(10)}#设置为遍历带有open('a.txt')asf:forlineinf:print(line)tupleunpackingfori,jin[(1,2),(3,4)]:print(i,j)>>>1234调用函数时,使用*解包封装实参deffunc(a,b,c):print(a,b,c)func(*[1,2,3])#将列表拆分[1,2,3]变成三个实参,分别对应传递给func函数生成器的a,b,c三个形参Python之禅说过,简单胜于复杂。鉴于上述代码中迭代器的复杂实现。Python提供了一个更pythonic的实现——生成器。生成器函数是包含yield关键字的函数(目前这个说法是正确的,后面会学习yield等语法,所以这个说法需要修正),生成器对象就是调用生成器函数。生成器实现将上述代码修改为生成器实现,如下:(1,2,3,4,5)print('第一个for')foriinins:print(i)print('thesecondfor')foriinins:print(i)print('最后...')>>>#9第一个for12345第二个for12345theend...上面代码中,iterable对象的iter方法并没有用短短几行就完成了前面Iterator迭代器的功能,点击Thumbs向上!yield关键字要理解上面的代码,你需要理解yield关键字。我们先看下面最简单的生成器函数实现deffunc():yield1yield2yield3ins1=func()ins2=func()print(func)print(ins1)print(ins2)foriinins1:print(i)foriinins1:print(i)print(next(ins2))print(next(ins2))print(next(ins2))print(next(ins2))>>>123123File"/home/disk/test/a.py",line18,inprint(next(ins2))StopIteration从上面的代码可以看出:func是一个函数,但是调用func会返回一个生成器对象,根据打印出来的地址,每次调用生成器函数,都会返回一个新的生成器对象。生成器对象类似于迭代器对象,可以通过for循环进行迭代,只能迭代一次,next调用会在生成器元素为空时抛出StopIteration异常。那么包含yield关键字的生成器函数体是如何执行的呢?请看下面的代码:```pythondeff_gen():#10print('start')yield1#11print('stop')yield2#12print('next')yield3#13print('end')foriinf_gen():#14print(i)>>>start1stop2next3end```从上面的代码及其打印结果,我们可以得出以下结论:#10显示,生成器函数的定义和普通函数一样,只是需要包含yield关键字——\#14for循环隐式调用next时,会执行到#11,printstart,然后将输出值1返回给for循环,print-for循环继续调用next,**executesfrom#11to#12**#,printsstop,然后将输出值2返回给for循环,prints——for循环继续调用next,**从#12执行到#13**#,打印next,然后将输出值3返回给for循环,print——for循环继续调用next,**从#13执行到函数的末尾**#,打印end和thenraise一个StopIteration,因为for循环捕获了异常,程序正常执行-**综上所述,yield有暂停的作用,每次迭代生成器,生成器函数体都会前进到yield语句并抛出yield之后的值(没有值抛出None)。生成器函数作为工厂函数,实现了可迭代对象中iter函数的功能,每次都可以产生一个新的迭代器实例。由于使用了特殊的yield关键字,它有了一个不同于迭代器的新名字——生成器,它实际上和迭代器一样**生成器表达式将列表推导中的[]改为(),这是一个生成器表达式。返回的是一个生成器对象。在导出一般用户列表但不需要一次生成所有值的场景下。gen=(iforiinrange(10))foriingen:print(i)foriingen:#只能消费一次,第二遍无输出print(i)print('theend...')>>>0123456789结束...itertoolsPython的内置模块itertools提供了很多对生成器的支持。这里举一个例子,其他支持请参考文档gen=itertools.count(1,2)#从1开始,步长为2,不断生成值>>>next(gen)1>>>next(gen)3>>>next(gen)5>>>next(gen)7>>>next(gen)9>>>next(gen)11yieldfrom关键字yieldfrom是python3中出现的新语法。3.yieldfrom语法启用委派生成器。deffunc():yieldfrom(iforiinrange(5))gen=func()foriingen:print(i)>>>01234如上所示,yieldfrom使用func作为委托生成器。for循环可以通过委托生成器函数(iforiinrange(5))直接迭代子生成器。但是仅仅这个技巧还远远不足以将yieldfrom作为一种新的语法添加到Python中。与上述代码的迭代内循环相比,新语法更重要的作用是委托生成器在调用者和子生成器之间建立了管道。通过生成器的send方法,可以将消息传递到管道的两端。如果用这种方法在程序层面控制线程行为,就会爆发出强大的能量,这就是所谓的协程。写在最后Notes迭代器和生成器很强大,但是在使用的时候还是有几点需要注意:迭代器要实现iter方法,虽然很多时候不实现这个方法页面并不会影响代码运行。实现这个方法主要有两个原因:迭代器协议规定需要实现这个方法是通过issubclass检查对象是否是迭代器,而不是将可迭代对象变成迭代器。有两个原因:这不符合迭代器协议,造成四维差异。可迭代对象应该是可遍历的。如果成为迭代器,则只能遍历一次。tips就我个人而言,我认为迭代器很有趣。os.walkos.walk迭代器可以深度遍历目录。这是一个大杀手。你应得的。去试试吧。iteriter可以接受两个位置参数:callable和flag。callable()可以连续输出值,如果等于flag就终止。下面是一个小例子gen=(iforiinrange(10))foriiniter(lambda:next(gen),4):#执行ntext(gen),并不断返回生成器中的值,如果等于4Stopprint(i)>>>0123theend...yield可以接收到值yield可以接收到send发送的值。在下面的代码中,#16处的send的值将传递给#15中的yield,然后赋值给res。deffunc():res=yield1#15print(res)f=func()f.send(None)#pre-excitationf.send(5)#16希望你能通过这个掌握装饰器的杀手级特性文章。欢迎来到我的个人博客:姚少敏的博客