Python进阶:全面解读高级特性之切片_第1页
Python进阶:全面解读高级特性之切片_第2页
Python进阶:全面解读高级特性之切片_第3页
已阅读5页,还剩14页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、众所周知,我们可以通过索引值(或称下标)来查找序列类型(如字符串、列表、元组 )中的单个元素,那么,如果要获取一个索引区间的元素该怎么办呢?切片( slice )就是一种截取索引片段的技术,借助切片技术,我们可以十分灵活地处理序列类型的对象。通常来说,切片的作用就是截取序列对象,然而,对于非序列对象,我们是否有办法做到切片操作呢?在使用切片的过程中,有什么要点值得重视,又有什么底层原理值得关注呢?本文将主要跟大家一起来探讨这些内容,希望我能与你共同学习进步。切片的基础用法列表是 Python中极为基础且重要的一种数据结构,也是最能发挥切片的用处的一种数据结构,所以在前两节,我将以列表为例介绍切

2、片的一些常见用法。首先是切片的书写形式: i : i+n : m ;其中, i 是切片的起始索引值,为列表首位时可省略;1,不允许为 0 ,当 m为负数时,列表翻转。注意:这些值都可以大于列表长度,不会报越界。i+n是切片的结束位置,为列表末位时可省略;m可以不提供,默认值是切片的基本含义是:从序列的第i 位索引起,向右取到后n位元素为止,按m间隔过滤。li= 1,4,5,6, 7, 9, 11,14,16# 以下写法都可以表示整个列表,其中X>= len(li)li0:X = li0: = li:X = li:= li: = li-X:X = li-X:li1:5 = 4,5,6,7#

3、从1起,取 5-1 位元素li1:5:2 =4,6 #从 1 起,取 5-1 位元素,按 2 间隔过滤li-1:= 16 #取倒数第一个元素li-4:-2 = 9, 11 #从倒数第四起,取-2-(-4)=2 位元素li:-2= li-len(li):-2=1,4,5,6,7,9,11# 从头开始,取 -2-(-len(li)=7位元素# 步长为负数时,列表先翻转,再截取li:-1=16,14,11,9,7,6,5,4,1# 翻转整个列表li:-2=16,11,7,5,1#翻转整个列表,再按 2 间隔过滤li:-5:-1=16,14,11,9# 翻转整个列表,取 -5-(-len(li)=4

4、位元素li:-5:-3=16,9#翻转整个列表,取 -5-(-len(li)=4 位元素,再按3间隔过滤# 切片的步长不可以为 0li:0#报错( ValueError:slicestepcannotbezero )上述的某些例子对于初学者(甚至很多老手)来说,可能还不好理解,但是它们都离不开切片的基本语法,所以为方便起见,我将它们也归入基础用法中。对于这些样例,我个人总结出两条经验:( 1)牢牢记住公式 i : i+n : m ,当出现缺省值时,通过想象把公式补全;( 2)索引为负且步长为正时,按倒数计算索引位置;索引为负且步长为负时,先翻转列表,再按倒数计算索引位置。切片的高级用法一般而言

5、,切片操作的返回结果是一个新的独立的序列( PS:也有例外,参见 Python 是否支持复制字符串呢?)。以列表为例,列表切片后得到的还是一个列表,占用新的内存地址。当取出切片的结果时,它是一个独立对象,因此,可以将其用于赋值操作,也可以用于其它传递值的场景。但是,切片只是浅拷贝,它拷贝的是原列表中元素的引用,所以,当存在变长对象的元素时,新列表将受制于原列表。li = 1, 2, 3, 4ls = li:li = ls # Trueid(li) = id(ls) # Falseli.append(li2:4) # 1, 2, 3, 4,3, 4ls.extend(ls2:4)# 1,2, 3

6、, 4, 3, 4# 下例等价于判断li长度是否大于 8if(li8:):print("notempty")else:print("empty")# 切片列表受制于原列表lo=1,1,1,2,3lp=lo:2#1,1,1lo1.append(1)#1,1,1,1,2,3lp#1,1,1,1由于可见,将切片结果取出,它可以作为独立对象使用,但是也要注意,是否取出了变长对象的元素。切片既可以作为独立对象被“取出”原序列,也可以留在原序列,作为一种占位符使用。不久前,我介绍了几种拼接字符串的方法(链接见文末),其中三种格式化类的拼接方法(即%、form at(

7、 ) 、te mplate )就是使用了占位符的思想。对于列表来说,使用切片作为占位符,同样能够实现拼接列表的效果。特别需要注意的是,给切片赋值的必须是可迭代对象。li=1,2,3,4# 在头部拼接li:0= 0#1,2,3,40,# 在末尾拼接lilen(li): = 5,7 # 0, 1, 2, 3, 4, 5, 7# 在中部拼接li6:6= 6# 0,1,2,3,4,5,6,7# 给切片赋值的必须是可迭代对象li-1:-1= 6#(报错,TypeError:can only assignaniterable)li:0=(9,)#9,0,1,2,3,4,5,6,7li:0=range(3)

8、#0, 1,2,9, 0,1,2, 3,4,5, 6, 7上述例子中,若将切片作为独立对象取出,那你会发现它们都是空列表,即li:0=lilen( li ):=li6:6= ,我将这种占位符称为“纯占位符 ”,对纯占位符赋值,并不会破坏原有的元素,只会在特定的索引位置中拼接进新的元素。删除纯占位符时,也不会影响列表中的元素。与“纯占位符”相对应,“非纯占位符 ”的切片是非空列表,对它进行操作(赋值与删除),将会影响原始列表。如果说纯占位符可以实现列表的拼接,那么,非纯占位符可以实现列表的替换。li = 1, 2, 3, 4# 不同位置的替换li:3=7,8,9#7,8,9,4li3:=5,6,

9、7#7,8,9,5,6,7li2:4='a','b'# 7,8,'a','b',6, 7# 非等长替换li2:4= 1,2,3,4#7,8,1,2,3,4,6,7li2:6= 'a'# 7,8,'a', 6,7# 删除元素del li2:3 # 7, 8, 6, 7切片占位符可以带步长,从而实现连续跨越性的替换或删除效果。需要注意的是,这种用法只支持等长替换。li = 1, 2, 3, 4, 5, 6li:2 = 'a','b','c' # '

10、a', 2, 'b', 4, 'c', 6li:2 = 0*3 # 0, 2, 0, 4, 0, 6li:2='w'#报错, attempttoassignsequenceofsize1toextendedsliceofsize3del li:2 # 2, 4, 6自定义对象实现切片功能切片是 Python 中最迷人最强大最 A mazing 的语言特性(几乎没有之一),以上两小节虽然介绍了切片的基础用法与高级用法,但这些还不足以充分地展露切片的魅力,所以,在接下来的两章节中,我们将聚焦于它的更高级用法。前两节内容都是基于原生的序列类型(

11、如字符串、列表、元组义其它对象(如字典)并让它支持切片呢? ),那么,我们是否可以定义自己的序列类型并让它支持切片语法呢?更进一步,我们是否可以自定3.1、魔术方法: get i te m()想要使自定义对象支持切片语法并不难,只需要在定义类的时候给它实现魔术方法_get item_()即可。所以,这里就先介绍一下这个方法。语法: object ._get ite m_(self, key)官方文档释义: Called to imple ment eva luat ion of selfkey . For sequence types, the accepted keys should be

12、integers and s lice objec ts . Note that the spec ia l interpre tation o f negat ive indexes (if the class wishes to e mulate a sequence t ype) is up to the _get item_() method. If key is of an inappropr iate type, TypeError may be raised; if of a value outs ide the set of indexes fo r the sequence

13、(after any spec ia l interpre tation of negat ive values) , IndexError should be raised. For mapping types, if key is miss ing (not in the conta iner ), KeyError should be raised.概括翻译一下:_get ite m_() 方法用于返回参数key 所对应的值,这个key可以是整型数值和切片对象,并且支持负数索引;如果 key不是以上两种类型,就会抛TypeError ;如果索引越界,会抛IndexError;如果定义的是

14、映射类型,当key参数不是其对象的键值时,则会抛KeyError。3.2、自定义序列实现切片功能接下来,我们定义一个简单的MyLis t ,并给它加上切片功能。(PS:仅作演示,不保证其它功能的完备性)。import numbersclass MyList():def _init_(self, anylist):self.data = anylistdef_len_(self):return len(self.data)def_getitem_(self,index):print("keyis: "+str(index)cls=type(self)ifisinstance(

15、index,slice):print("datais:" +str(self.dataindex)returncls(self.dataindex)elifisinstance(index,numbers.Integral):returnself.dataindexelse:msg ="cls._name_indicesmust be integers"raiseTypeError(msg.format(cls=cls)l=MyList("My","name","is","Python

16、 猫 ")# 输出结果:key is : 3Python 猫key is : slice(None, 2, None)datais :'My','name'<_main_.MyListobjectat0x0000019CD83A7A90key is: hi>Traceback (most recent call last):.TypeError:MyListindicesmust be integersor slices从输出结果来看,自定义的MyLis t既支持按索引查找,也支持切片操作,这正是我们的目的。3.3、自定义字典实现切片功能切

17、片是序列类型的特性,所以在上例中,我们不需要写切片的具体实现逻辑。但是,对于其它非序列类型的自定义对象,就得自己实现切片逻辑。以自定义字典为例(PS:仅作演示,不保证其它功能的完备性):classMyDict():def_init_(self):defself.data=_len_(self):defreturnlen(self.data)append(self,item):defself.datalen(self)=item_getitem_(self,key):ifisinstance(key,int):returnself.datakeyifisinstance(key,slice):s

18、licedkeys=list(self.data.keys()keyreturnk: self.datak for k in slicedkeyselse:raiseTypeErrord=MyDict()d.append("My")d.append("name")d.append("is")d.append("Python猫 ")print(d2)print(d:2)print(d-4:-2)print(d'hi')# 输出结果:is0:'My',1:'name'0:

19、'My',1:'name'Traceback(mostrecentcalllast):.TypeError上例的关键点在于将字典的键值取出,并对键值的列表做切片处理,其妙处在于,不用担心索引越界和负数索引,将字典切片转换成了字典键值的切片,最终实现目的。迭代器实现切片功能好了,介绍完一般的自定义对象如何实现切片功能,这里将迎来另一类非同一般的对象。迭代器是 Python 中独特的一种高级对象,它本身不具备切片功能,然而若能将它用于切片,这便仿佛是锦上添花,能达到如虎添翼的效果。所以,本节将隆重地介绍迭代器如何实现切片功能。4.1、迭代与迭代器首先,有几个基本概念

20、要澄清:迭代、可迭代对象、迭代器。迭代 是一种遍历容器类型对象(例如字符串、列表、字典等等)的方式,例如,我们说迭代一个字符串“abc”,指的就是从左往右依次地、逐个地取出它的全部字符的过程。( PS:汉语中迭代一词有循环反复、层层递进的意思,但Python中此词要理解成单向水平线性的,如果你不熟悉它,我建议直接将其理解为遍历。)那么,怎么写出迭代操作的指令呢?最通用的书写语法就是for循环。# for 循环实现迭代过程forcharin"abc":print(char,end="")#输出结果: abcfor 循环可以实现迭代的过程,但是,并非所有对象

21、都可以用于for 循环,例如,上例中若将字符串“abc”换成任意整型数字,则会报错:'int' objec t is not iterable .这句报错中的单词 “iterable ”指的是“可迭代的”,即 int 类型不是可迭代的。而字符串(string )类型是可迭代的,同样地,列表、元组、字典等类型,都是可迭代的。那怎么判断一个对象是否可迭代呢?为什么它们是可迭代的呢?怎么让一个对象可迭代呢?要使一个对象可迭代,就要实现可迭代协议,即需要实现_iter_()魔术方法,换言之,只要实现了这个魔术方法的对象都是可迭代对象。那怎么判断一个对象是否实现了这个方法呢?除了上述的f

22、or循环外,我还知道四种方法:# 方法 1 : dir() 查看 _iter_dir(2)#没有,略dir("abc")#有,略# 方法 2 : isinstance() 判断importcollectionsisinstance(2,collections.Iterable)#Falseisinstance("abc", collections.Iterable) # True# 方法 3 : hasattr() 判断hasattr(2,"_iter_")#Falsehasattr("abc","_it

23、er_")#True# 方法 4 :用 iter() 查看是否报错iter(2)#报错: 'int'objectisnotiterableiter("abc")#<str_iteratorat0x1e2396d8f28>#PS :判断是否可迭代,还可以查看是否实现_getitem_ ,为方便描述,本文从略。这几种方法中最值得一提的是i ter() 方法,它是 Python的内置方法,其作用是将可迭代对象变成迭代器。这句话可以解析出两层意思:(1)可迭代对象跟迭代器是两种东西;(2 )可迭代对象能变成迭代器。实际上,迭代器必然是可迭代对象

24、,但可迭代对象不一定是迭代器。两者有多大的区别呢?如上图蓝圈所示,普通可迭代对象与迭代器的最关键区别可概括为:一同两不同,所谓“一同”,即两者都是可迭代的(_iter_ ),所谓“两不同”,即可迭代对象在转化为迭代器后,它会丢失一些属性(_geti te m_),同时也增加一些属性(_next_ )。首先看看增加的属性_next_, 它是迭代器之所以是迭代器的关键,事实上,我们正是把同时实现了_i ter_ 方法 和 _next_方法的对象定义为迭代器的。有了多出来的这个属性,可迭代对象不需要借助外部的for 循环语法,就能实现自我的迭代/ 遍历过程。我发明了两个概念来描述这两种遍历过程(PS

25、 :为了易理解,这里称遍历,实际也可称为迭代):它遍历指的是通过外部语法而实现的遍历,自遍历指的是通过自身方法实现的遍历。借助这两个概念,我们说,可迭代对象就是能被“它遍历”的对象,而迭代器是在此基础上,还能做到“自遍历”的对象。ob1 = "abc" ob2 = iter("abc") ob3 = iter("abc")# ob1 它遍历for i in ob1:print(i, end = " ")for i in ob1:print(i, end = " ")# ob1 自遍历ob1._n

26、ext_()# 报错:# a b c# a b c'str' object has no attribute '_next_'# ob2 它遍历for i in ob2:print(i, end = " ")for i in ob2:print(i, end = " ")# a b c# 无输出# ob2 自遍历 ob2._next_()# ob3 自遍历 ob3._next_() ob3._next_() ob3._next_() ob3._next_()# 报错: StopIteration# a# b# c# 报错:

27、 StopIteration通过上述例子可看出,迭代器的优势在于支持自遍历,同时,它的特点是单向非循环的,一旦完成遍历,再次调用就会报错。对此,我想到一个比方:普通可迭代对象就像是子弹匣,它遍历就是取出子弹,在完成操作后又装回去,所以可以反复遍历(即多次调用for 循环,返回相同结果);而迭代器就像是装载了子弹匣且不可拆卸的枪,进行它遍历或者自遍历都是发射子弹,这是消耗性的遍历,是无法复用的(即遍历会有尽头)。写了这么多,稍微小结一下:迭代是一种遍历元素的方式,按照实现方式划分,有外部迭代与内部迭代两种,支持外部迭代(它遍历)的对象就是可迭代对象,而同时还支持内部迭代(自遍历)的对象就是迭代器

28、;按照消费方式划分,可分为复用型迭代与一次性迭代,普通可迭代对象是复用型的,而迭代器是一次性的。4.2、迭代器切片前面提到了 “一同两不同”,最后的不同是,普通可迭代对象在转化成迭代器的过程中会丢失一些属性,其中关键的属性是_get item_。在前一节中,我已经介绍了这个魔术方法,并用它实现了自定义对象的切片特性。那么问题来了:为啥迭代器不继承这个属性呢?首先,迭代器使用的是消耗型的遍历,这意味着它充满不确定性,即其长度与索引键值对是动态衰减的,所以很难get 到它的 ite m ,也就不再需要_get ite m_属性了。其次,若强行给迭代器加上这个属性,这并不合理,正所谓强扭的瓜不甜 由

29、此,新的问题来了:既然会丢失这么重要的属性(还包括其它未标识的属性),为什么还要使用迭代器呢?这个问题的答案在于,迭代器拥有不可替代的强大的有用的功能,使得Python要如此设计它。限于篇幅,此处不再展开,后续我会专门填坑此话题。还没完,死缠烂打的问题来了:能否令迭代器拥有这个属性呢,即令迭代器继续支持切片呢?hi=" 欢迎关注公众号:Python 猫 "it=iter(hi)# 普通切片hi-7:#Python 猫# 反例:迭代器切片it-7:#报错: 'str_iterator'objectisnotsubscriptable迭代器因为缺少_getite

30、m_,因此不能使用普通的切片语法。想要实现切片,无非两种思路:一是自己造轮子,写实现的逻辑;二是找到封装好的轮子。Python 的 itertools模块就是我们要找的轮子,用它提供的方法可轻松实现迭代器切片。import itertools# 例 1 :简易迭代器s =iter("123456789")forxinitertools.islice(s,xprint(x,end = "")forinitertools.islice(s,print(x, end = " ")# 例 2 :斐波那契数列迭代器classFib():def_

31、init_(self):self.a,self.b2,6):2,#输出:34566):#输出: 9=1,1def_iter_(self):while True:yieldself.aself.a,self.b=self.b, self.a+ self.bf=iter(Fib()forxinitertools.islice(f,2,6):xprint(x, end = " ")2,#输出:2358forinitertools.islice(f,6):print(x,end = "")#输出: 3455 89 144itertools 模块的 islice(

32、 ) 方法将迭代器与切片完美结合,终于回答了前面的问题。然而,迭代器切片跟普通切片相比,前者有很多局限性。首先,这个方法不是“纯函数”(纯函数需遵守 “相同输入得到相同输出 ”的原则);其次,它只支持正向切片,且不支持负数索引,这都是由迭代器的损耗性所决定的。那么,我不禁要问:i tertools模块的切片方法用了什么实现逻辑呢?下方是官网提供的源码:def islice(iterable, *args):# islice('ABCDEFG', 2) -> A B# islice('ABCDEFG', 2, 4) -> C D#islice('

33、;ABCDEFG',2,None)->CDEFG#islice('ABCDEFG',0,None,2)->ACEGs= slice(*args)# 索引区间是 0,sys.maxsize ,默认步长是 1start,stop,step=s.startor0,s.stoporsys.maxsize,s.stepor 1it=iter(range(start,stop,step)try:nexti= next(it)exceptStopIteration:#Consume *iterable*uptothe *start*position.fori,elementinzip(range(start),iterable):passreturntry:fori,elementinenumerate(iterable):ifi = nexti:yieldelementnexti= next(it)exceptStopIteration

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论