導讀:切片系列文章連續寫了三篇,本文是對它們作的彙總。爲何要把序列文章合併呢?在此說明一下,本文毫不是簡單地將它們作了合併,主要是修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接作了大量改動,如此一來,本文結構的完整性與內容的質量都獲得了很好的保證。python
衆所周知,咱們能夠經過索引值(或稱下標)來查找序列類型(如字符串、列表、元組...)中的單個元素,那麼,若是要獲取一個索引區間的元素該怎麼辦呢?編程
切片(slice)就是一種截取索引片斷的技術,藉助切片技術,咱們能夠十分靈活地處理序列類型的對象。一般來講,切片的做用就是截取序列對象,然而,對於非序列對象,咱們是否有辦法作到切片操做呢?在使用切片的過程當中,有什麼要點值得重視,又有什麼底層原理值得關注呢?本文將主要跟你們一塊兒來探討這些內容,但願我能與你共同窗習進步。數據結構
列表是 Python 中極爲基礎且重要的一種數據結構,也是最能發揮切片的用處的一種數據結構,因此在前兩節,我將以列表爲例介紹切片的一些常見用法。app
首先是切片的書寫形式:[i : i+n : m] ;其中,i 是切片的起始索引值,爲列表首位時可省略;i+n 是切片的結束位置,爲列表末位時可省略;m 能夠不提供,默認值是1,不容許爲0 ,當m爲負數時,列表翻轉。注意:這些值均可以大於列表長度,不會報越界。異步
切片的基本含義是:從序列的第i位索引發,向右取到後n位元素爲止,按m間隔過濾 。ide
li = [1, 4, 5, 6, 7, 9, 11, 14, 16] # 如下寫法均可以表示整個列表,其中 X >= len(li) li[0:X] == li[0:] == li[:X] == li[:] == li[::] == li[-X:X] == li[-X:] li[1:5] == [4,5,6,7] # 從1起,取5-1位元素 li[1: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位元素 li[:-5:-3] == [16,9] # 翻轉整個列表,取-5-(-len(li))=4位元素,再按3間隔過濾 # 切片的步長不能夠爲0 li[::0] # 報錯(ValueError: slice step cannot be zero)
上述的某些例子對於初學者(甚至不少老手)來講,可能還很差理解,可是它們都離不開切片的基本語法,因此爲方便起見,我將它們也納入基礎用法中。函數
對於這些樣例,我我的總結出兩條經驗:源碼分析
(1)緊緊記住公式[i : i+n : m]
,當出現缺省值時,經過想象把公式補全;性能
(2)索引爲負且步長爲正時,按倒數計算索引位置;索引爲負且步長爲負時,先翻轉列表,再按倒數計算索引位置。學習
通常而言,切片操做的返回結果是一個新的獨立的序列(PS:也有例外,參見《Python是否支持複製字符串呢?》)。以列表爲例,列表切片後獲得的仍是一個列表,佔用新的內存地址。
當取出切片的結果時,它是一個獨立對象,所以,能夠將其用於賦值操做,也能夠用於其它傳遞值的場景。可是,切片只是淺拷貝 ,它拷貝的是原列表中元素的引用,因此,當存在變長對象的元素時,新列表將受制於原列表。
li = [1, 2, 3, 4] ls = li[::] li == ls # True id(li) == id(ls) # False li.append(li[2:4]) # [1, 2, 3, 4, [3, 4]] ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4] # 下例等價於判斷li長度是否大於8 if(li[8:]): print("not empty") else: print("empty") # 切片列表受制於原列表 lo = [1,[1,1],2,3] lp = lo[:2] # [1, [1, 1]] lo[1].append(1) # [1, [1, 1, 1], 2, 3] lp # [1, [1, 1, 1]]
因爲可見,將切片結果取出,它能夠做爲獨立對象使用,可是也要注意,是否取出了變長對象的元素。
切片既能夠做爲獨立對象被「取出」原序列,也能夠留在原序列,做爲一種佔位符使用。
不久前,我介紹了幾種拼接字符串的方法(連接見文末),其中三種格式化類的拼接方法(即 %、format()、template)就是使用了佔位符的思想。對於列表來講,使用切片做爲佔位符,一樣可以實現拼接列表的效果。特別須要注意的是,給切片賦值的必須是可迭代對象。
li = [1, 2, 3, 4] # 在頭部拼接 li[:0] = [0] # [0, 1, 2, 3, 4] # 在末尾拼接 li[len(li):] = [5,7] # [0, 1, 2, 3, 4, 5, 7] # 在中部拼接 li[6:6] = [6] # [0, 1, 2, 3, 4, 5, 6, 7] # 給切片賦值的必須是可迭代對象 li[-1:-1] = 6 # (報錯,TypeError: can only assign an iterable) li[:0] = (9,) # [9, 0, 1, 2, 3, 4, 5, 6, 7] li[:0] = range(3) # [0, 1, 2, 9, 0, 1, 2, 3, 4, 5, 6, 7]
上述例子中,若將切片做爲獨立對象取出,那你會發現它們都是空列表,即 li[:0]==li[len(li):]==li[6:6]==[]
,我將這種佔位符稱爲「純佔位符」,對純佔位符賦值,並不會破壞原有的元素,只會在特定的索引位置中拼接進新的元素。刪除純佔位符時,也不會影響列表中的元素。
與「純佔位符」相對應,「非純佔位符」的切片是非空列表,對它進行操做(賦值與刪除),將會影響原始列表。若是說純佔位符能夠實現列表的拼接,那麼,非純佔位符能夠實現列表的替換。
li = [1, 2, 3, 4] # 不一樣位置的替換 li[:3] = [7,8,9] # [7, 8, 9, 4] li[3:] = [5,6,7] # [7, 8, 9, 5, 6, 7] li[2:4] = ['a','b'] # [7, 8, 'a', 'b', 6, 7] # 非等長替換 li[2:4] = [1,2,3,4] # [7, 8, 1, 2, 3, 4, 6, 7] li[2:6] = ['a'] # [7, 8, 'a', 6, 7] # 刪除元素 del li[2:3] # [7, 8, 6, 7]
切片佔位符能夠帶步長,從而實現連續跨越性的替換或刪除效果。須要注意的是,這種用法只支持等長替換。
li = [1, 2, 3, 4, 5, 6] li[::2] = ['a','b','c'] # ['a', 2, 'b', 4, 'c', 6] li[::2] = [0]*3 # [0, 2, 0, 4, 0, 6] li[::2] = ['w'] # 報錯,attempt to assign sequence of size 1 to extended slice of size 3 del li[::2] # [2, 4, 6]
切片是 Python 中最迷人最強大最 Amazing 的語言特性(幾乎沒有之一),以上兩小節雖然介紹了切片的基礎用法與高級用法,但這些還不足以充分地展露切片的魅力,因此,在接下來的兩章節中,咱們將聚焦於它的更高級用法。
前兩節內容都是基於原生的序列類型(如字符串、列表、元組......),那麼,咱們是否能夠定義本身的序列類型並讓它支持切片語法呢?更進一步,咱們是否能夠自定義其它對象(如字典)並讓它支持切片呢?
__getitem__()
想要使自定義對象支持切片語法並不難,只須要在定義類的時候給它實現魔術方法 __getitem__()
便可。因此,這裏就先介紹一下這個方法。
語法: object.__getitem__(self, key)
官方文檔釋義:Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects. Note that the special interpretation of negative indexes (if the class wishes to emulate a sequence type) is up to the __getitem__()
method. If key is of an inappropriate type, TypeError may be raised; if of a value outside the set of indexes for the sequence (after any special interpretation of negative values), IndexError should be raised. For mapping types, if key is missing (not in the container), KeyError should be raised.
歸納翻譯一下:__getitem__()
方法用於返回參數 key 所對應的值,這個 key 能夠是整型數值和切片對象,而且支持負數索引;若是 key 不是以上兩種類型,就會拋 TypeError;若是索引越界,會拋 IndexError ;若是定義的是映射類型,當 key 參數不是其對象的鍵值時,則會拋 KeyError 。
接下來,咱們定義一個簡單的 MyList ,並給它加上切片功能。(PS:僅做演示,不保證其它功能的完備性)。
import numbers class MyList(): def __init__(self, anylist): self.data = anylist def __len__(self): return len(self.data) def __getitem__(self, index): print("key is : " + str(index)) cls = type(self) if isinstance(index, slice): print("data is : " + str(self.data[index])) return cls(self.data[index]) elif isinstance(index, numbers.Integral): return self.data[index] else: msg = "{cls.__name__} indices must be integers" raise TypeError(msg.format(cls=cls)) l = MyList(["My", "name", "is", "Python貓"]) ### 輸出結果: key is : 3 Python貓 key is : slice(None, 2, None) data is : ['My', 'name'] <__main__.MyList object at 0x0000019CD83A7A90> key is : hi Traceback (most recent call last): ... TypeError: MyList indices must be integers or slices
從輸出結果來看,自定義的 MyList 既支持按索引查找,也支持切片操做,這正是咱們的目的。
切片是序列類型的特性,因此在上例中,咱們不須要寫切片的具體實現邏輯。可是,對於其它非序列類型的自定義對象,就得本身實現切片邏輯。以自定義字典爲例(PS:僅做演示,不保證其它功能的完備性):
class MyDict(): def __init__(self): self.data = {} def __len__(self): return len(self.data) def append(self, item): self.data[len(self)] = item def __getitem__(self, key): if isinstance(key, int): return self.data[key] if isinstance(key, slice): slicedkeys = list(self.data.keys())[key] return {k: self.data[k] for k in slicedkeys} else: raise TypeError d = MyDict() d.append("My") d.append("name") d.append("is") d.append("Python貓") print(d[2]) print(d[:2]) print(d[-4:-2]) print(d['hi']) ### 輸出結果: is {0: 'My', 1: 'name'} {0: 'My', 1: 'name'} Traceback (most recent call last): ... TypeError
上例的關鍵點在於將字典的鍵值取出,並對鍵值的列表作切片處理,其妙處在於,不用擔憂索引越界和負數索引,將字典切片轉換成了字典鍵值的切片,最終實現目的。
好了,介紹完通常的自定義對象如何實現切片功能,這裏將迎來另外一類非同通常的對象。
迭代器是 Python 中獨特的一種高級對象,它自己不具有切片功能,然而若能將它用於切片,這便彷彿是錦上添花,能達到如虎添翼的效果。因此,本節將隆重地介紹迭代器如何實現切片功能。
首先,有幾個基本概念要澄清:迭代、可迭代對象、迭代器。
迭代
是一種遍歷容器類型對象(例如字符串、列表、字典等等)的方式,例如,咱們說迭代一個字符串「abc」,指的就是從左往右依次地、逐個地取出它的所有字符的過程。(PS:漢語中迭代一詞有循環反覆、層層遞進的意思,但 Python 中此詞要理解成單向水平線性 的,若是你不熟悉它,我建議直接將其理解爲遍歷。)
那麼,怎麼寫出迭代操做的指令呢?最通用的書寫語法就是 for 循環。
# for循環實現迭代過程 for char in "abc": print(char, end=" ") # 輸出結果:a b c
for 循環能夠實現迭代的過程,可是,並不是全部對象均可以用於 for 循環,例如,上例中若將字符串「abc」換成任意整型數字,則會報錯: 'int' object is not iterable .
這句報錯中的單詞「iterable」指的是「可迭代的」,即 int 類型不是可迭代的。而字符串(string)類型是可迭代的,一樣地,列表、元組、字典等類型,都是可迭代的。
那怎麼判斷一個對象是否可迭代呢?爲何它們是可迭代的呢?怎麼讓一個對象可迭代呢?
要使一個對象可迭代,就要實現可迭代協議,即須要實現__iter__()
魔術方法,換言之,只要實現了這個魔術方法的對象都是可迭代對象。
那怎麼判斷一個對象是否實現了這個方法呢?除了上述的 for 循環外,我還知道四種方法:
# 方法1:dir()查看__iter__ dir(2) # 沒有,略 dir("abc") # 有,略 # 方法2:isinstance()判斷 import collections isinstance(2, collections.Iterable) # False isinstance("abc", collections.Iterable) # True # 方法3:hasattr()判斷 hasattr(2,"__iter__") # False hasattr("abc","__iter__") # True # 方法4:用iter()查看是否報錯 iter(2) # 報錯:'int' object is not iterable iter("abc") # <str_iterator at 0x1e2396d8f28> ### PS:判斷是否可迭代,還能夠查看是否實現__getitem__,爲方便描述,本文從略。
這幾種方法中最值得一提的是 iter() 方法,它是 Python 的內置方法,其做用是將可迭代對象變成迭代器 。這句話能夠解析出兩層意思:(1)可迭代對象跟迭代器是兩種東西;(2)可迭代對象能變成迭代器。
實際上,迭代器必然是可迭代對象,但可迭代對象不必定是迭代器。二者有多大的區別呢?
如上圖藍圈所示,普通可迭代對象與迭代器的最關鍵區別可歸納爲:一同兩不一樣 ,所謂「一同」,即二者都是可迭代的(__iter__
),所謂「兩不一樣」,便可迭代對象在轉化爲迭代器後,它會丟失一些屬性(__getitem__
),同時也增長一些屬性(__next__
)。
首先看看增長的屬性 __next__
, 它是迭代器之因此是迭代器的關鍵,事實上,咱們正是把同時實現了 __iter__
方法 和 __next__
方法的對象定義爲迭代器的。
有了多出來的這個屬性,可迭代對象不須要藉助外部的 for 循環語法,就能實現自個人迭代/遍歷過程。我發明了兩個概念來描述這兩種遍歷過程(PS:爲了易理解,這裏稱遍歷,實際也可稱爲迭代):它遍歷
指的是經過外部語法而實現的遍歷,自遍歷
指的是經過自身方法實現的遍歷。
藉助這兩個概念,咱們說,可迭代對象就是能被「它遍歷」的對象,而迭代器是在此基礎上,還能作到「自遍歷」的對象。
ob1 = "abc" ob2 = iter("abc") ob3 = iter("abc") # ob1它遍歷 for i in ob1: print(i, end = " ") # a b c for i in ob1: print(i, end = " ") # a b c # ob1自遍歷 ob1.__next__() # 報錯: 'str' object has no attribute '__next__' # ob2它遍歷 for i in ob2: print(i, end = " ") # a b c for i in ob2: print(i, end = " ") # 無輸出 # ob2自遍歷 ob2.__next__() # 報錯:StopIteration # ob3自遍歷 ob3.__next__() # a ob3.__next__() # b ob3.__next__() # c ob3.__next__() # 報錯:StopIteration
經過上述例子可看出,迭代器的優點在於支持自遍歷,同時,它的特色是單向非循環的,一旦完成遍歷,再次調用就會報錯。
對此,我想到一個比方:普通可迭代對象就像是子彈匣,它遍歷就是取出子彈,在完成操做後又裝回去,因此能夠反覆遍歷(即屢次調用for循環,返回相同結果);而迭代器就像是裝載了子彈匣且不可拆卸的槍,進行它遍歷或者自遍歷都是發射子彈,這是消耗性的遍歷,是沒法複用的(即遍歷會有盡頭)。
寫了這麼多,稍微小結一下:迭代是一種遍歷元素的方式,按照實現方式劃分,有外部迭代與內部迭代兩種,支持外部迭代(它遍歷)的對象就是可迭代對象,而同時還支持內部迭代(自遍歷)的對象就是迭代器;按照消費方式劃分,可分爲複用型迭代與一次性迭代,普通可迭代對象是複用型的,而迭代器是一次性的。
前面提到了「一同兩不一樣」,最後的不一樣是,普通可迭代對象在轉化成迭代器的過程當中會丟失一些屬性,其中關鍵的屬性是 __getitem__
。在前一節中,我已經介紹了這個魔術方法,並用它實現了自定義對象的切片特性。
那麼問題來了:爲何迭代器不繼承這個屬性呢?
首先,迭代器使用的是消耗型的遍歷,這意味着它充滿不肯定性,即其長度與索引鍵值對是動態衰減的,因此很難 get 到它的 item ,也就再也不須要 __getitem__
屬性了。其次,若強行給迭代器加上這個屬性,這並不合理,正所謂強扭的瓜不甜......
由此,新的問題來了:既然會丟失這麼重要的屬性(還包括其它未標識的屬性),爲何還要使用迭代器呢?
這個問題的答案在於,迭代器擁有不可替代的強大的有用的功能,使得 Python 要如此設計它。限於篇幅,此處再也不展開,後續我會專門填坑此話題。
還沒完,死纏爛打的問題來了:可否令迭代器擁有這個屬性呢,即令迭代器繼續支持切片呢?
hi = "歡迎關注公衆號:Python貓" it = iter(hi) # 普通切片 hi[-7:] # Python貓 # 反例:迭代器切片 it[-7:] # 報錯:'str_iterator' object is not subscriptable
迭代器由於缺乏__getitem__
,所以不能使用普通的切片語法。想要實現切片,無非兩種思路:一是本身造輪子,寫實現的邏輯;二是找到封裝好的輪子。
Python 的 itertools 模塊就是咱們要找的輪子,用它提供的方法可輕鬆實現迭代器切片。
import itertools # 例1:簡易迭代器 s = iter("123456789") for x in itertools.islice(s, 2, 6): print(x, end = " ") # 輸出:3 4 5 6 for x in itertools.islice(s, 2, 6): print(x, end = " ") # 輸出:9 # 例2:斐波那契數列迭代器 class Fib(): def __init__(self): self.a, self.b = 1, 1 def __iter__(self): while True: yield self.a self.a, self.b = self.b, self.a + self.b f = iter(Fib()) for x in itertools.islice(f, 2, 6): print(x, end = " ") # 輸出:2 3 5 8 for x in itertools.islice(f, 2, 6): print(x, end = " ") # 輸出:34 55 89 144
itertools 模塊的 islice() 方法將迭代器與切片完美結合,終於回答了前面的問題。然而,迭代器切片跟普通切片相比,前者有不少侷限性。首先,這個方法不是「純函數」(純函數需遵照「相同輸入獲得相同輸出」的原則);其次,它只支持正向切片,且不支持負數索引,這都是由迭代器的損耗性所決定的。
那麼,我不由要問:itertools 模塊的切片方法用了什麼實現邏輯呢?下方是官網提供的源碼:
def islice(iterable, *args): # islice('ABCDEFG', 2) --> A B # islice('ABCDEFG', 2, 4) --> C D # islice('ABCDEFG', 2, None) --> C D E F G # islice('ABCDEFG', 0, None, 2) --> A C E G s = slice(*args) # 索引區間是[0,sys.maxsize],默認步長是1 start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1 it = iter(range(start, stop, step)) try: nexti = next(it) except StopIteration: # Consume *iterable* up to the *start* position. for i, element in zip(range(start), iterable): pass return try: for i, element in enumerate(iterable): if i == nexti: yield element nexti = next(it) except StopIteration: # Consume to *stop*. for i, element in zip(range(i + 1, stop), iterable): pass
islice() 方法的索引方向是受限的,但它也提供了一種可能性:即容許你對一個無窮的(在系統支持範圍內)迭代器進行切片的能力。這是迭代器切片最具想象力的用途場景。
除此以外,迭代器切片還有一個很實在的應用場景:讀取文件對象中給定行數範圍的數據。
咱們知道,從文件中讀取內容主要有兩種方法(參見以前關於文件讀寫的文章):read() 適合讀取內容較少的狀況,或者是須要一次性處理所有內容的狀況;而 readlines() 適用性更廣,由於它是迭代地讀取內容,既減小內存壓力,又方便逐行對數據處理。
雖然 readlines() 有迭代讀取的優點,但它是從頭至尾逐行讀取,若文件有幾千行,而咱們只想要讀取少數特定行(例如第1000-1009行),那它仍是效率過低了。考慮到文件對象自然就是迭代器 ,咱們可使用迭代器切片先行截取,而後再處理,如此效率將大大地提高。
# test.txt 文件內容 ''' 貓 Python貓 python is a cat. this is the end. ''' from itertools import islice with open('test.txt','r',encoding='utf-8') as f: print(hasattr(f, "__next__")) # 判斷是否迭代器 content = islice(f, 2, 4) for line in content: print(line.strip()) ### 輸出結果: True python is a cat. this is the end.
本節內容較多,簡單回顧一下:迭代器是一種特殊的可迭代對象,可用於它遍歷與自遍歷,但遍歷過程是損耗型的,不具有循環複用性,所以,迭代器自己不支持切片操做;經過藉助 itertools 模塊,咱們能實現迭代器切片,將二者的優點相結合,其主要用途在於截取大型迭代器(如無限數列、超大文件等等)的片斷,實現精準的處理,從而大大地提高性能與效率。
最後總結一下,切片是 Python 的一種高級特性,經常使用於截取序列類型的元素,但並不侷限於此,本文主要介紹了它的基礎用法、高級用法(如佔位符用法)、自定義對象切片、以及迭代器切片等使用內容。除此以外,切片還有更廣闊多樣的使用場景,例如 Numpy 的多維切片、內存視圖切片、異步迭代器切片等等,都值得咱們去探索一番,今限於篇幅而沒法細說,歡迎關注公衆號「Python貓 」,之後咱們慢慢學習之。
切片系列(原單篇):
相關連接:
官方文檔getitem用法:http://t.cn/EbzoZyp
切片賦值的源碼分析:http://t.cn/EbzSaoZ
官網itertools模塊介紹:http://t.cn/EbNc0ot
來自Kenneth Reitz大神的建議:避免沒必要要的面向對象編程
給Python學習者的文件讀寫指南(含基礎與進階,建議收藏)
-----------------
本文原創並首發於微·信·公·衆·號【Python貓】,後臺回覆「愛學習」,免費得到20+本精選電子書。