2018-12-31更新声明:切片系列文章分三篇写成,现在合并为一篇。合并后修复了一些严重的错误(比如自定义序列切片的部分),在文本结构和章节连接上做了很多改动。原系列的单篇文章就不删了,毕竟也是作为单独的文章。在此声明,请阅读改进版——Python进阶:高级特性切片全面解读!https://mp.weixin.qq.com/s/IR...在前两篇关于Python切片的文章中,我们学习了切片的基本用法、高级用法、对切片的误解,以及自定义对象如何实现切片的用法(详见相关链接见文末)。本文是切片系列的第三篇,主要内容是迭代器切片。迭代器是Python独有的高级特性,切片也是高级特性。两者结合起来会是什么结果呢?1.迭代和迭代器首先,有几个基本概念需要明确:迭代、可迭代对象和迭代器。迭代是一种遍历容器类型对象(如字符串、列表、字典等)的方式。比如我们说迭代一个字符串“abc”,意思是从左到右的字符过程,一个一个的取出来。(PS:迭代这个词中文意思是循环重复,逐层递进,但是这个词在Python中应该理解为单向水平线性,不熟悉的建议直接理解为遍历。)那么,迭代操作的指令怎么写呢?最常见的编写语法是for循环。#for循环实现对charin"abc"的迭代过程:print(char,end="")#输出结果:abcfor循环可以实现迭代过程,但并不是所有的对象都可以for循环,例如上面的In本例中,如果将字符串“abc”替换为任意整数,则会报错:'int'objectisnotiterable。这个报错中的“iterable”指的是“iterable”,即int类型是不可迭代的。字符串(string)类型是可迭代的,类似的,列表、元组、字典等类型也是可迭代的。那么如何判断一个对象是否可迭代呢?为什么它们是可迭代的?如何使对象可迭代?要使一个对象可迭代,就需要实现可迭代协议,即实现__iter__()魔术方法。也就是说,只要实现了这个魔法方法的对象就是可迭代对象。那么如何判断一个对象是否实现了这个方法呢?除了上面的for循环,我知道还有其他四种方法:#方法一:dir()查看__iter__dir(2)#不,略dir("abc")#是,略#方法二:isinstance()judgmentimportcollectionsisinstance(2,collections.Iterable)#Falseisinstance("abc",collections.Iterable)#True#方法三:hasattr()judgehasattr(2,"__iter__")#Falsehasattr("abc","__iter__")#True#方法四:使用iter()检查是否报错iter(2)#Error:'int'objectisnotiterableiter("abc")####PS:判断是否报错是可迭代的,也可以查看是否实现了__getitem__,为了描述方便,本文省略。这些方法中最值得一提的是iter()方法,它是Python中的一个内置方法,可以将可迭代对象变成迭代器。这句话可以解析成两层意思:(1)可迭代对象和迭代器是两个东西;(2)可迭代对象可以成为迭代器。其实迭代器一定是可迭代对象,但可迭代对象不一定是迭代器。两者的区别有多大?如上图蓝色圆圈所示,普通可迭代对象和迭代器最关键的区别可以概括为:一和二不同,所谓“一起”,即两者都是可迭代的(__iter__),所以-所谓“二异”,也就是一个可迭代对象转换为迭代器后,会失去一些属性(__getitem__),同时也会增加一些属性(__next__)。先看添加的属性__next__,这是迭代器为什么是迭代器的关键。实际上,我们将一个同时实现了__iter__方法和__next__方法的对象定义为一个迭代器。有了这个额外的属性,可迭代对象就可以在不借助外部for循环语法的情况下实现自己的迭代/遍历过程。我发明了两个概念来描述这两个遍历过程(PS:为了方便理解,这里叫遍历,其实也可以叫迭代):遍历指的是通过外部语法实现的遍历,自遍历指的是通过自己的方法实现的遍历。借助这两个概念,我们说可迭代对象是可以被它“遍历”的对象,迭代器是在此基础上也可以“自遍历”的对象。ob1="abc"ob2=iter("abc")ob3=iter("abc")#ob1迭代foriinob1:print(i,end="")#abcforiinob1:print(i,end="")#abc#ob1self-traversalob1.__next__()#error:'str'objecthasnoattribute'__next__'#ob2ittraversesforiinob2:print(i,end="")#abcforiinob2:print(i,end="")#Nooutput#ob2self-traversalob2.__next__()#error:StopIteration#ob3self-traversalob3.__next__()#aob3.__next__()#bob3.__next__()#cob3.__next__()#Error:StopIteration从上面的例子可以看出,迭代器的优点是支持自遍历。同时,它的特点是单向无环。一旦遍历完成,再次调用会报错。对此,我想到了一个类比:普通的可迭代对象就像是子弹杂志。它的遍历是把子弹取出来,操作完成后再放回去,所以可以反复遍历(即多次调用for循环,返回相同的结果);迭代器就像一把装有弹匣的不可拆卸的枪。穿越它或自我穿越它就是发射子弹。这是一次消耗性遍历,不能重复使用(即遍历会有结束)。写了这么多,我简单总结一下:迭代是一种遍历元素的方式。按实现方式分,有外部迭代和内部迭代两种。支持外部迭代(自遍历)的对象是可迭代对象,同时支持内部迭代(自遍历)的对象是迭代器;按消耗方式分为可复用迭代和一次性迭代。普通的可迭代对象是可重用的,而迭代器是一次性的。2.IteratorSlicing前面说了“一同二异”。最后的区别是,普通的可迭代对象在转化为迭代器的过程中会丢失一些属性,而关键属性是__getitem__。在《Python进阶:自定义对象实现切片功能》中,我介绍了这个神奇的方法,并用它来实现自定义对象的切片功能。那么问题来了:迭代器为什么不继承这个属性呢?首先,迭代器使用消费型遍历,这意味着它充满了不确定性,即它的长度和索引键值对是动态衰减的,所以很难得到它的item,__getitem__不再需要的属性。其次,强行把这个属性加到迭代器上是不合理的,正所谓扭瓜不甜……于是,新的问题又出现了:既然这么重要的属性(包括其他不明属性),为什么还要用迭代器呢?这个问题的答案是迭代器具有不可替代的强大和有用的特性,使得Python如此设计它。限于篇幅,这里就不展开了,以后会专门填写这个话题。还没完,追猎者的问题来了:迭代器能不能有这个属性,即使迭代器继续支持切片?hi="欢迎关注公众号:Pythoncat"it=iter(hi)#普通slicehi[-7:]#Pythoncat#反例:Iteratorsliceit[-7:]#Error:'str_iterator'objectisAnotsubscriptableiterator不能使用普通的切片语法,因为它缺少__getitem__。想要实现切片,无外乎两种思路:一种是自己造轮子,写实现的逻辑;另一种是找一个包装好的轮子。Python的itertools模块就是我们要找的轮子,它提供了方便实现迭代器切片的方法。importitertools#示例1:简单迭代器s=iter("123456789")forxinitertools.islice(s,2,6):print(x,end="")#Output:3456forxinitertools.islice(s,2,6):print(x,end="")#Output:9#Example2:FibonaccisequenceiteratorclassFib():def__init__(self):self.a,self.b=1,1def__iter__(self):whileTrue:yieldself.aself.a,self.b=self.b,self.a+self.bf=iter(Fib())forxinitertools。islice(f,2,6):print(x,end="")#输出:2358forxinitertools.islice(f,2,6):print(x,end="")#output:345589144itertools模块的islice()方法完美结合了迭代器和切片,最终回答了前面的问题。然而,迭代器切片与普通切片相比有很多限制。首先,这个方法不是“纯函数”(纯函数必须遵守“同输入同输出”的原则,前面《来自Kenneth Reitz大神的建议:避免不必要的面向对象编程》讲过);其次,它只支持正向切片,不支持负索引,这都是由迭代器的有损决定的。那么,我不禁要问:itertools模块的slice方法是用什么实现逻辑的?下面是官网提供的源码:defislice(iterable,*args):#islice('ABCDEFG',2)-->AB#islice('ABCDEFG',2,4)-->CD#islice('ABCDEFG',2,None)-->CDEFG#islice('ABCDEFG',0,None,2)-->ACEGs=slice(*args)#索引区间为[0,sys.maxsize],默认步长为1start,stop,step=s.startor0,s.stoporsys.maxsize,s.stepor1it=iter(range(start,stop,step))try:nexti=next(it)exceptStopIteration:#消耗*iterable*直到*start*位置。fori,elementinzip(range(start),iterable):passreturntry:fori,elementinenumerate(iterable):ifi==nexti:yieldelementnexti=next(it)除了StopIteration:#Consumeto*停止*。fori,elementinzip(range(i+1,stop),iterable):passislice()方法的索引方向是有限制的,但它也提供了一种可能性:让你切片一个无限(到系统支持的范围)迭代器。这是迭代器切片最有想象力的使用场景。此外,迭代器切片还有一个非常实用的应用场景:读取文件对象中给定行范围内的数据。在《给Python学习者的文件读写指南(含基础与进阶,建议收藏)》中,我介绍了几种从文件中读取内容的方法:readline()比较没用,用处不大;read()适用于读取内容较少,或者需要一次处理所有内容的情况;而readlines()用得更多,也更灵活。每次迭代读取内容,既减少了内存压力,也方便逐行处理数据。readlines()虽然有迭代读取的优势,但是是从头到尾逐行读取。如果文件有几千行,而我们只想读取特定的几行(比如1000-1009行),那还是太低效了。考虑到文件对象天生就是迭代器,我们可以使用迭代器切片先拦截再处理,这样会大大提高效率。#test.txt文件内容'''catPythoncatpythonisacat.thisisend.'''fromitertoolsimportislicewithopen('test.txt','r',encoding='utf-8')asf:print(hasattr(f,"__next__"))#判断是否迭代器content=islice(f,2,4)forlineincontent:print(line.strip())###输出结果:Truepython是一个cat.thisistheend.3.总结好了,今天的学习到此结束。总结一下:迭代器是一种特殊的可迭代对象,可以对其进行遍历和自遍历,但是遍历过程是有损的。不具备复用性,因此迭代器本身不支持切片操作;通过使用itertools模块,我们可以实现迭代器切片,结合两者的优点。其主要目的是拦截大型迭代器(如无限数组、大型文件等)实现精确处理,从而大大提高性能和效率。切片系列:《Python进阶:切片的误区与高级用法》《Python进阶:自定义对象实现切片功能》相关链接:《官网的itertools模块介绍》《来自Kenneth Reitz大神的建议:避免不必要的面向对象编程》《给Python学习者的文件读写指南(含基础与进阶,建议收藏)》----------------本文首发于微信公众号【蟒猫】,后台回复“爱了”学习”,免费获取20多本精选电子书。