導讀:切片系列文章連續寫了三篇,本文是對它們做的彙總。為什麼要把序列文章合并呢?在此說明一下,本文絕不是簡單地将它們做了合并,主要是修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動,如此一來,本文結構的完整性與内容的品質都得到了很好的保證。
衆所周知,我們可以通過索引值(或稱下标)來查找序列類型(如字元串、清單、元組…)中的單個元素,那麼,如果要擷取一個索引區間的元素該怎麼辦呢?
切片(slice)就是一種截取索引片段的技術,借助切片技術,我們可以十分靈活地處理序列類型的對象。通常來說,切片的作用就是截取序列對象,然而,對于非序列對象,我們是否有辦法做到切片操作呢?在使用切片的過程中,有什麼要點值得重視,又有什麼底層原理值得關注呢?本文将主要跟大家一起來探讨這些内容,希望我能與你共同學習進步。
1、切片的基礎用法
清單是 Python 中極為基礎且重要的一種資料結構,也是最能發揮切片的用處的一種資料結構,是以在前兩節,我将以清單為例介紹切片的一些常見用法。
首先是切片的書寫形式:[i : i+n : m] ;其中,i 是切片的起始索引值,為清單首位時可省略;i+n 是切片的結束位置,為清單末位時可省略;m 可以不提供,預設值是1,不允許為0 ,當m為負數時,清單翻轉。注意:這些值都可以大于清單長度,不會報越界。
切片的基本含義是:從序列的第i位索引起,向右取到後n位元素為止,按m間隔過濾 。
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)索引為負且步長為正時,按倒數計算索引位置;索引為負且步長為負時,先翻轉清單,再按倒數計算索引位置。
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]
3、自定義對象實作切片功能
切片是 Python 中最迷人最強大最 Amazing 的語言特性(幾乎沒有之一),以上兩小節雖然介紹了切片的基礎用法與進階用法,但這些還不足以充分地展露切片的魅力,是以,在接下來的兩章節中,我們将聚焦于它的更進階用法。
前兩節内容都是基于原生的序列類型(如字元串、清單、元組……),那麼,我們是否可以定義自己的序列類型并讓它支援切片文法呢?更進一步,我們是否可以自定義其它對象(如字典)并讓它支援切片呢?
3.1、魔術方法:`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 。
3.2、自定義序列實作切片功能
接下來,我們定義一個簡單的 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 既支援按索引查找,也支援切片操作,這正是我們的目的。
3.3、自定義字典實作切片功能
切片是序列類型的特性,是以在上例中,我們不需要寫切片的具體實作邏輯。但是,對于其它非序列類型的自定義對象,就得自己實作切片邏輯。以自定義字典為例(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
上例的關鍵點在于将字典的鍵值取出,并對鍵值的清單做切片處理,其妙處在于,不用擔心索引越界和負數索引,将字典切片轉換成了字典鍵值的切片,最終實作目的。
4、疊代器實作切片功能
好了,介紹完一般的自定義對象如何實作切片功能,這裡将迎來另一類非同一般的對象。
疊代器是 Python 中獨特的一種進階對象,它本身不具備切片功能,然而若能将它用于切片,這便仿佛是錦上添花,能達到如虎添翼的效果。是以,本節将隆重地介紹疊代器如何實作切片功能。
4.1、疊代與疊代器
首先,有幾個基本概念要澄清:疊代、可疊代對象、疊代器。
疊代
是一種周遊容器類型對象(例如字元串、清單、字典等等)的方式,例如,我們說疊代一個字元串“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循環,傳回相同結果);而疊代器就像是裝載了子彈匣且不可拆卸的槍,進行它周遊或者自周遊都是發射子彈,這是消耗性的周遊,是無法複用的(即周遊會有盡頭)。
寫了這麼多,稍微小結一下:疊代是一種周遊元素的方式,按照實作方式劃分,有外部疊代與内部疊代兩種,支援外部疊代(它周遊)的對象就是可疊代對象,而同時還支援内部疊代(自周遊)的對象就是疊代器;按照消費方式劃分,可分為複用型疊代與一次性疊代,普通可疊代對象是複用型的,而疊代器是一次性的。
4.2、疊代器切片
前面提到了“一同兩不同”,最後的不同是,普通可疊代對象在轉化成疊代器的過程中會丢失一些屬性,其中關鍵的屬性是
__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 子產品,我們能實作疊代器切片,将兩者的優勢相結合,其主要用途在于截取大型疊代器(如無限數列、超大檔案等等)的片段,實作精準的處理,進而大大地提升性能與效率。
5、小結
最後總結一下,切片是 Python 的一種進階特性,常用于截取序列類型的元素,但并不局限于此,本文主要介紹了它的基礎用法、進階用法(如占位符用法)、自定義對象切片、以及疊代器切片等使用内容。除此之外,切片還有更廣闊多樣的使用場景,例如 Numpy 的多元切片、記憶體視圖切片、異步疊代器切片等等,都值得我們去探索一番,今限于篇幅而無法細說,歡迎關注公衆号“Python貓 ”,以後我們慢慢學習之。
切片系列(原單篇):
Python進階:切片的誤區與進階用法
Python進階:自定義對象實作切片功能
Python進階:疊代器與疊代器切片
相關連結:
官方文檔getitem用法:http://t.cn/EbzoZyp
切片指派的源碼分析:http://t.cn/EbzSaoZ
官網itertools子產品介紹:http://t.cn/EbNc0ot
Python是否支援複制字元串呢?
來自Kenneth Reitz大神的建議:避免不必要的面向對象程式設計
給Python學習者的檔案讀寫指南(含基礎與進階,建議收藏)
詳解Python拼接字元串的七種方式
-----------------
本文原創并首發于微信公衆号【Python貓】,背景回複“愛學習”,免費獲得20+本精選電子書。
# Python進階:深入淺出解讀進階特性之切片!
衆所周知,我們可以通過索引值(或稱下标)來查找序列類型(如字元串、清單、元組...)中的單個元素,那麼,如果要擷取一個索引區間的元素該怎麼辦呢?
## 1、切片的基礎用法
首先是切片的書寫形式:[i : i+n : m] ;其中,i 是切片的起始索引值,為清單首位時可省略;i+n 是切片的結束位置,為清單末位時可省略;m 可以不提供,預設值是1,**不允許為0** ,當m為負數時,清單翻轉。注意:這些值都可以大于清單長度,不會報越界。
切片的基本含義是:**從序列的第i位索引起,向右取到後n位元素為止,按m間隔過濾** 。
```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間隔過濾
# 切片的步長不可以為0li[::0] # 報錯(ValueError: slice step cannot be zero)```
(1)牢牢記住公式`[i : i+n : m]` ,當出現預設值時,通過想象把公式補全;
## 2、切片的進階用法
一般而言,切片操作的傳回結果是一個新的獨立的序列(PS:也有例外,參見《[Python是否支援複制字元串呢?](https://mp.weixin.qq.com/s/PsY8Iec1EeGDKKnTNU5CNw)》)。以清單為例,清單切片後得到的還是一個清單,占用新的記憶體位址。
當取出切片的結果時,它是一個獨立對象,是以,可以将其用于指派操作,也可以用于其它傳遞值的場景。但是,**切片隻是淺拷貝** ,它拷貝的是原清單中元素的引用,是以,當存在變長對象的元素時,新清單将受制于原清單。
```li = [1, 2, 3, 4]ls = li[::]
li == ls # Trueid(li) == id(ls) # Falseli.append(li[2:4]) # [1, 2, 3, 4, [3, 4]]ls.extend(ls[2:4]) # [1, 2, 3, 4, 3, 4]
# 下例等價于判斷li長度是否大于8if(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]]```
```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[: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]```
## 3、自定義對象實作切片功能
前兩節内容都是基于原生的序列類型(如字元串、清單、元組......),那麼,我們是否可以定義自己的序列類型并讓它支援切片文法呢?更進一步,我們是否可以自定義其它對象(如字典)并讓它支援切片呢?
### 3.1、魔術方法:`__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 。
### 3.2、自定義序列實作切片功能
```pythonimport 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 : 3Python貓key is : slice(None, 2, None)data is : ['My', 'name']<__main__.MyList object at 0x0000019CD83A7A90>key is : hiTraceback (most recent call last):...TypeError: MyList indices must be integers or slices```
### 3.3、自定義字典實作切片功能
```pythonclass 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```
## 4、疊代器實作切片功能
### 4.1、疊代與疊代器
`疊代` 是一種周遊容器類型對象(例如字元串、清單、字典等等)的方式,例如,我們說疊代一個字元串“abc”,指的就是從左往右依次地、逐個地取出它的全部字元的過程。(PS:漢語中疊代一詞有循環反複、層層遞進的意思,但 Python 中此詞要了解成**單向水準線性** 的,如果你不熟悉它,我建議直接将其了解為周遊。)
```python# for循環實作疊代過程for char in "abc": print(char, end=" ")# 輸出結果:a b c```
要使一個對象可疊代,就要實作可疊代協定,即需要實作`__iter__()` 魔術方法,換言之,隻要實作了這個魔術方法的對象都是可疊代對象。
```python# 方法1:dir()檢視__iter__dir(2) # 沒有,略dir("abc") # 有,略
# 方法2:isinstance()判斷import collectionsisinstance(2, collections.Iterable) # Falseisinstance("abc", collections.Iterable) # True
# 方法3:hasattr()判斷hasattr(2,"__iter__") # Falsehasattr("abc","__iter__") # True
# 方法4:用iter()檢視是否報錯iter(2) # 報錯:'int' object is not iterableiter("abc") # <str_iterator at 0x1e2396d8f28>
### PS:判斷是否可疊代,還可以檢視是否實作__getitem__,為友善描述,本文從略。```
這幾種方法中最值得一提的是 iter() 方法,它是 Python 的内置方法,其作用是**将可疊代對象變成疊代器** 。這句話可以解析出兩層意思:(1)可疊代對象跟疊代器是兩種東西;(2)可疊代對象能變成疊代器。

如上圖藍圈所示,普通可疊代對象與疊代器的最關鍵差別可概括為:**一同兩不同** ,所謂“一同”,即兩者都是可疊代的(`__iter__`),所謂“兩不同”,即可疊代對象在轉化為疊代器後,它會丢失一些屬性(`__getitem__`),同時也增加一些屬性(`__next__`)。
首先看看增加的屬性 `__next__` , 它是疊代器之是以是疊代器的關鍵,事實上,我們正是把同時實作了 `__iter__` 方法 和 `__next__` 方法的對象定義為疊代器的。
有了多出來的這個屬性,可疊代對象不需要借助外部的 for 循環文法,就能實作自我的疊代/周遊過程。我發明了兩個概念來描述這兩種周遊過程(PS:為了易了解,這裡稱周遊,實際也可稱為疊代):`它周遊` 指的是通過外部文法而實作的周遊,`自周遊` 指的是通過自身方法實作的周遊。
```pythonob1 = "abc"ob2 = iter("abc")ob3 = iter("abc")
# ob1它周遊for i in ob1: print(i, end = " ") # a b cfor 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__() # aob3.__next__() # bob3.__next__() # cob3.__next__() # 報錯:StopIteration```
寫了這麼多,稍微小結一下:**疊代是一種周遊元素的方式,按照實作方式劃分,有外部疊代與内部疊代兩種,支援外部疊代(它周遊)的對象就是可疊代對象,而同時還支援内部疊代(自周遊)的對象就是疊代器;按照消費方式劃分,可分為複用型疊代與一次性疊代,普通可疊代對象是複用型的,而疊代器是一次性的。**
### 4.2、疊代器切片
前面提到了“一同兩不同”,最後的不同是,普通可疊代對象在轉化成疊代器的過程中會丢失一些屬性,其中關鍵的屬性是 `__getitem__` 。在前一節中,我已經介紹了這個魔術方法,并用它實作了自定義對象的切片特性。
首先,疊代器使用的是消耗型的周遊,這意味着它充滿不确定性,即其長度與索引鍵值對是動态衰減的,是以很難 get 到它的 item ,也就不再需要 `__getitem__` 屬性了。其次,若強行給疊代器加上這個屬性,這并不合理,正所謂強扭的瓜不甜......
```pythonhi = "歡迎關注公衆号:Python貓"it = iter(hi)
# 普通切片hi[-7:] # Python貓
# 反例:疊代器切片it[-7:] # 報錯:'str_iterator' object is not subscriptable```
疊代器因為缺少`__getitem__` ,是以不能使用普通的切片文法。想要實作切片,無非兩種思路:一是自己造輪子,寫實作的邏輯;二是找到封裝好的輪子。
```pythonimport itertools
# 例1:簡易疊代器s = iter("123456789")for x in itertools.islice(s, 2, 6): print(x, end = " ") # 輸出:3 4 5 6for 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.bf = iter(Fib())for x in itertools.islice(f, 2, 6): print(x, end = " ") # 輸出:2 3 5 8for x in itertools.islice(f, 2, 6): print(x, end = " ") # 輸出:34 55 89 144```
```pythondef 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```
雖然 readlines() 有疊代讀取的優勢,但它是從頭到尾逐行讀取,若檔案有幾千行,而我們隻想要讀取少數特定行(例如第1000-1009行),那它還是效率太低了。考慮到**檔案對象天然就是疊代器** ,我們可以使用疊代器切片先行截取,然後再處理,如此效率将大大地提升。
```python# test.txt 檔案内容'''貓Python貓python is a cat.this is the end.'''
from itertools import islicewith 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())### 輸出結果:Truepython is a cat.this is the end.```
## 5、小結
最後總結一下,切片是 Python 的一種進階特性,常用于截取序列類型的元素,但并不局限于此,本文主要介紹了它的基礎用法、進階用法(如占位符用法)、自定義對象切片、以及疊代器切片等使用内容。除此之外,切片還有更廣闊多樣的使用場景,例如 Numpy 的多元切片、記憶體視圖切片、異步疊代器切片等等,都值得我們去探索一番,今限于篇幅而無法細說,歡迎關注公衆号“**Python貓** ”,以後我們慢慢學習之。
**切片系列(原單篇):**
[Python進階:切片的誤區與進階用法](https://mp.weixin.qq.com/s/fwZnvcQ_u-fYLFeQaYkJLw)
[Python進階:自定義對象實作切片功能](https://mp.weixin.qq.com/s/QTodsriWW_gESvmJPD1EYg)
[Python進階:疊代器與疊代器切片](https://mp.weixin.qq.com/s/DIhwoJE3o0kKw3kzZKysag)
**相關連結:**
[Python是否支援複制字元串呢?](https://mp.weixin.qq.com/s/PsY8Iec1EeGDKKnTNU5CNw)
[來自Kenneth Reitz大神的建議:避免不必要的面向對象程式設計](https://mp.weixin.qq.com/s/2l_erf55IMvT4Tz0AGWCUQ)
[給Python學習者的檔案讀寫指南(含基礎與進階,建議收藏)](https://mp.weixin.qq.com/s/Md07VoaULda7qnMO4ob7Ww)
[詳解Python拼接字元串的七種方式](https://mp.weixin.qq.com/s/Whrd6NiD4Y2Z-YSCy4XJ1w)
\-----------------