切片雖然是 Python 中很常使用的機制, 不過你可能沒有真的瞭解它, 比如說:
>>> 'hello'[::-1]
'olleh'
依照許多教學的說法, 省略起點和終點時, 會以 0 和 序列的長度代入, 但你如果真的用這兩個值代入:
>>> 'hello'[0:5:-1]
''
卻會得到空字串, 顯然實際上並不是這樣運作。
slice 類別的物件才是切片的實際運作機制
其實切片的語法實際上會變換成 slice
類別的物件, 例如:
>>> 'hello'[0:3:1]
'hel'
其實就是:
>>> 'hello'[slice(0, 3, 1)]
'hel'
也就是:
-------->
| h | e | l | l | o |
0 1 2 3 4
如果起點或是終點是負數, 會再加上序列長度值作為運作值, 也就是:
| h | e | l | l | o |
-5 -4 -3 -2 -1 切片中的負數
+5
0 1 2 3 4 實際的索引
這也是為什麼會說負數的索引是從尾端往回計數的原因。例如:
>>> 'hello'[slice(0, -1, 1)]
'hell'
實際上是:
>>> 'hello'[slice(0, -1 + 5, 1)]
'hell'
也就是:
---------------->
| h | e | l | l | o |
-5 -4 -3 -2 -1 切片中的負數
+5
0 1 2 3 4 實際的索引
使用 slice.indices() 查看實際切片範圍
如果想要知道到底切出來的是哪一段範圍, 可以使用 slice 類別的 indices() 方法。你可以先建立 slice
物件, 傳入序列的長度叫用 indices()
方法, 就會傳回一個序組, 告訴你起點、終點以及間距。例如以剛剛長度為 5 個字元的 "hello" 來說:
>>> s = slice(0, -1, 1)
>>> s.indices(len('hello'))
(0, 4, 1)
你可以看到 'hello[0:-1:1]
實際上是 'hello[0:4:1]'
:
-5 -4 -3 -2 -1 切片中的負數
---------------->
| h | e | l | l | o |
0 1 2 3 4 實際的索引
實際上切片的運作就是先取得 indices()
方法傳回的序組, 再依照序組內的起點、終點、間隔運作。
要注意的是, 由 indices()
方法傳回的序組中, 負數就是指索引 0 左邊的位置, 而不是從尾端往回數的位置:
| h | e | l | l | o |
-1 0 1 2 3 4 5
負的間隔值會逆向取資料
如果間隔值是負數, 就會變成反向從序列中取資料, 所以起點位置一定要在終點的右邊, 否則就會取出空的序列, 例如:
>>> 'hello'[1:3:-1]
''
>>> 'hello'[slice(1, 3, -1)]
''
一開始取資料起點就已經在終點左邊了, 所以結束取資料, 傳回空的序列:
起點 終點
v v
| h | e | l | l | o |
-1 0 1 2 3 4 5
結 開
尾 頭
<--負的間距
其實你只要把 slice
類別的 indices()
方法傳回序組內的三個數值當成是 range()
運算的三個參數 就可以知道會得到哪一段範圍的資料了。比如說:
>>> 'hello'[slice(-1, 2, -1)]
'ol'
之所以會得到這樣的結果, 是因為:
>>> s1 = slice(-1, 2, -1)
>>> s1.indices(len('hello'))
(4, 2, -1)
會從索引 4 開始往回, 所以會取出索引 4,3 位置的資料, 但不會取得終點索引 2 的資料:
<----
| h | e | l | l | o |
-1 0 1 2 3 4 5
結 開
尾 頭
<--負的間距
這跟從 range
得到的結果是一樣的:
>>> for i in range(*s1.indices(len('hello'))):
... print(i)
4
3
省略起點、終點時預設值是 None
當你在切片中省略起點或是終點時, 預設值是 None
, 而間距的正負還會影響如何解譯 None
。實際運作時會依據目前取資料的方向, 起點代入取資料開頭位置索引、終點則代入取資料結尾處再下一個位置的索引。
當間距是正值時, 是從左往右取資料, 所以開頭是索引 0, 結尾處再下一個位置的索引就是序列的長度;但當間距是負值時, 是從右往左取資料, 所以開頭的索引是序列的長度減 1, 而結尾處再下一個位置是最左端再往左一個位置的索引, 也就是 0 - 1, 為 -1:
正的間距-->
開 結
頭 尾
| h | e | l | l | o |
-1 0 1 2 3 4 5
結 開
尾 頭
<--負的間距
我們可以測試看看:
>>> s1 = slice(None, None, 1)
>>> s1.indices(len('hello'))
(0, 5, 1)
>>> s1 = slice(None, None, -1)
>>> s1.indices(len('hello'))
(4, -1, -1)
再強調一次, indices()
傳回的序組中, 起點或是終點的負數就是指索引 0 左邊的位置, 不會像是切片裡的負值會再自動加上序列的長度。
<----------------
| h | e | l | l | o |
-1 0 1 2 3 4 5
結 開
尾 頭
<--負的間距
這也就是為什麼可以使用 [::-1]
取得反轉的序列:
>>> 'hello'[::-1]
'olleh'
因為它實際的運作就是從索引位置 4 往回取資料, 一直到到達索引位置 -1 前面為止。
<----------------
| h | e | l | l | o |
-1 0 1 2 3 4 5
結 開
尾 頭
<--負的間距
正向取資料的時候就是一般教學所提到的, 省略起點預設是 0, 省略終點預設會是序列長度;但反向取資料時, 就要反過來看, 省略起點時預設是序列長度 - 1, 省略終點時預設則是 -1, 這需要多練習就會習慣。例如:
>>> 'hello'[:2:-1]
'ol'
因為省略起點, 所以起點就是序列的長度減 1, 本例就是 5 - 1, 也就是 4, 所以從索引 4,3 取資料, 得到從尾端開始的 2 個字元:
<----
| h | e | l | l | o |
-1 0 1 2 3 4 5
結 開
尾 頭
<--負的間距
又例如:
>>> 'hello'[2::-1]
'leh'
因為省略了終點, 終點就是 -1, 所以會從索引 2 往回取值到最左邊, 得到 3 個字元:
<--------
| h | e | l | l | o |
-1 0 1 2 3 4 5
結 開
尾 頭
<--負的間距
瞭解以上的說明後, 再看到什麼奇怪的切片寫法, 都可以迎刃而解了。
Top comments (0)