DEV Community

codemee
codemee

Posted on • Edited on

1

為什麼 [::-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

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

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs