DEV Community

codemee
codemee

Posted on • Edited on

為什麼 [::-1] 可以反轉序列 (sequence)?

切片雖然是 Python 中很常使用的機制, 不過你可能沒有真的瞭解它, 比如說:

>>> 'hello'[::-1]
'olleh'
Enter fullscreen mode Exit fullscreen mode

依照許多教學的說法, 省略起點和終點時, 會以 0 和 序列的長度代入, 但你如果真的用這兩個值代入:

>>> 'hello'[0:5:-1]
''
Enter fullscreen mode Exit fullscreen mode

卻會得到空字串, 顯然實際上並不是這樣運作。

slice 類別的物件才是切片的實際運作機制

其實切片的語法實際上會變換成 slice 類別的物件, 例如:

>>> 'hello'[0:3:1]
'hel'
Enter fullscreen mode Exit fullscreen mode

其實就是:

>>> 'hello'[slice(0, 3, 1)]
'hel'
Enter fullscreen mode Exit fullscreen mode

也就是:

  -------->
| h | e | l | l | o |
  0   1   2   3   4 
Enter fullscreen mode Exit fullscreen mode

如果起點或是終點是負數, 會再加上序列長度值作為運作值, 也就是:

| h | e | l | l | o |
 -5  -4  -3  -2  -1   切片中的負數
                      +5
  0   1   2   3   4   實際的索引
Enter fullscreen mode Exit fullscreen mode

這也是為什麼會說負數的索引是從尾端往回計數的原因。例如:

>>> 'hello'[slice(0, -1, 1)]
'hell'
Enter fullscreen mode Exit fullscreen mode

實際上是:

>>> 'hello'[slice(0, -1 + 5, 1)]
'hell'
Enter fullscreen mode Exit fullscreen mode

也就是:

  ---------------->
| h | e | l | l | o |
 -5  -4  -3  -2  -1   切片中的負數
                      +5
  0   1   2   3   4   實際的索引
Enter fullscreen mode Exit fullscreen mode

使用 slice.indices() 查看實際切片範圍

如果想要知道到底切出來的是哪一段範圍, 可以使用 slice 類別的 indices() 方法。你可以先建立 slice 物件, 傳入序列的長度叫用 indices() 方法, 就會傳回一個序組, 告訴你起點、終點以及間距。例如以剛剛長度為 5 個字元的 "hello" 來說:

>>> s = slice(0, -1, 1)
>>> s.indices(len('hello'))
(0, 4, 1)
Enter fullscreen mode Exit fullscreen mode

你可以看到 'hello[0:-1:1] 實際上是 'hello[0:4:1]'

 -5  -4  -3  -2  -1   切片中的負數
  ---------------->
| h | e | l | l | o |
  0   1   2   3   4   實際的索引
Enter fullscreen mode Exit fullscreen mode

實際上切片的運作就是先取得 indices() 方法傳回的序組, 再依照序組內的起點、終點、間隔運作。

要注意的是, 由 indices() 方法傳回的序組中, 負數就是指索引 0 左邊的位置, 而不是從尾端往回數的位置:

  | h | e | l | l | o |
-1  0   1   2   3   4   5
Enter fullscreen mode Exit fullscreen mode

負的間隔值會逆向取資料

如果間隔值是負數, 就會變成反向從序列中取資料, 所以起點位置一定要在終點的右邊, 否則就會取出空的序列, 例如:

>>> 'hello'[1:3:-1]
''

>>> 'hello'[slice(1, 3, -1)]
''
Enter fullscreen mode Exit fullscreen mode

一開始取資料起點就已經在終點左邊了, 所以結束取資料, 傳回空的序列:

        起點    終點
        v       v
  | h | e | l | l | o |
-1  0   1   2   3   4   5
    結              開
    尾              頭
                      <--負的間距
Enter fullscreen mode Exit fullscreen mode

其實你只要把 slice 類別的 indices() 方法傳回序組內的三個數值當成是 range() 運算的三個參數 就可以知道會得到哪一段範圍的資料了。比如說:

>>> 'hello'[slice(-1, 2, -1)]
'ol'
Enter fullscreen mode Exit fullscreen mode

之所以會得到這樣的結果, 是因為:

>>> s1 = slice(-1, 2, -1)
>>> s1.indices(len('hello'))
(4, 2, -1)
Enter fullscreen mode Exit fullscreen mode

會從索引 4 開始往回, 所以會取出索引 4,3 位置的資料, 但不會取得終點索引 2 的資料:

                <----
  | h | e | l | l | o |
-1  0   1   2   3   4   5
    結              開
    尾              頭
                      <--負的間距
Enter fullscreen mode Exit fullscreen mode

這跟從 range 得到的結果是一樣的:

>>> for i in range(*s1.indices(len('hello'))):
...     print(i)
4
3
Enter fullscreen mode Exit fullscreen mode

省略起點、終點時預設值是 None

當你在切片中省略起點或是終點時, 預設值是 None, 而間距的正負還會影響如何解譯 None。實際運作時會依據目前取資料的方向, 起點代入取資料開頭位置索引、終點則代入取資料結尾處再下一個位置的索引。

當間距是正值時, 是從左往右取資料, 所以開頭是索引 0, 結尾處再下一個位置的索引就是序列的長度;但當間距是負值時, 是從右往左取資料, 所以開頭的索引是序列的長度減 1, 而結尾處再下一個位置是最左端再往左一個位置的索引, 也就是 0 - 1, 為 -1:

正的間距-->
          開              結
          頭              尾
        | h | e | l | l | o |
      -1  0   1   2   3   4   5
          結              開
          尾              頭
                            <--負的間距
Enter fullscreen mode Exit fullscreen mode

我們可以測試看看:

>>> s1 = slice(None, None, 1)
>>> s1.indices(len('hello'))
(0, 5, 1)

>>> s1 = slice(None, None, -1)
>>> s1.indices(len('hello'))
(4, -1, -1)
Enter fullscreen mode Exit fullscreen mode

再強調一次, indices() 傳回的序組中, 起點或是終點的負數就是指索引 0 左邊的位置, 不會像是切片裡的負值會再自動加上序列的長度。

    <----------------
  | h | e | l | l | o |
-1  0   1   2   3   4   5
    結              開
    尾              頭
                      <--負的間距
Enter fullscreen mode Exit fullscreen mode

這也就是為什麼可以使用 [::-1] 取得反轉的序列:

>>> 'hello'[::-1]
'olleh'
Enter fullscreen mode Exit fullscreen mode

因為它實際的運作就是從索引位置 4 往回取資料, 一直到到達索引位置 -1 前面為止。

    <----------------
  | h | e | l | l | o |
-1  0   1   2   3   4   5
    結              開
    尾              頭
                      <--負的間距
Enter fullscreen mode Exit fullscreen mode

正向取資料的時候就是一般教學所提到的, 省略起點預設是 0, 省略終點預設會是序列長度;但反向取資料時, 就要反過來看, 省略起點時預設是序列長度 - 1, 省略終點時預設則是 -1, 這需要多練習就會習慣。例如:

>>> 'hello'[:2:-1]
'ol'
Enter fullscreen mode Exit fullscreen mode

因為省略起點, 所以起點就是序列的長度減 1, 本例就是 5 - 1, 也就是 4, 所以從索引 4,3 取資料, 得到從尾端開始的 2 個字元:

                <----
  | h | e | l | l | o |
-1  0   1   2   3   4   5
    結              開
    尾              頭
                      <--負的間距
Enter fullscreen mode Exit fullscreen mode

又例如:

>>> 'hello'[2::-1]
'leh'
Enter fullscreen mode Exit fullscreen mode

因為省略了終點, 終點就是 -1, 所以會從索引 2 往回取值到最左邊, 得到 3 個字元:

    <--------
  | h | e | l | l | o |
-1  0   1   2   3   4   5
    結              開
    尾              頭
                      <--負的間距
Enter fullscreen mode Exit fullscreen mode

瞭解以上的說明後, 再看到什麼奇怪的切片寫法, 都可以迎刃而解了。

Top comments (0)