為什麼 [::-1] 可以反轉序列 (sequence)？
Tags: python
Published: 2023-08-01 15:07:08

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

```python
>>> 'hello'[::-1]
'olleh'
```

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

```python
>>> 'hello'[0:5:-1]
''
```

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

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

其實[切片的語法](https://docs.python.org/3/reference/expressions.html?highlight=slicing#slicings)實際上會[變換成 `slice` 類別的物件](https://docs.python.org/3/reference/datamodel.html#index-97), 例如：

```python
>>> 'hello'[0:3:1]
'hel'
```

其實就是：

```python
>>> 'hello'[slice(0, 3, 1)]
'hel'
```

也就是：

```
  -------->
| h | e | l | l | o |
  0   1   2   3   4 
```


如果起點或是終點是負數, 會再[加上序列長度值作為運作值](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations), 也就是：

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

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

```python
>>> 'hello'[slice(0, -1, 1)]
'hell'
```

實際上是：

```python
>>> '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() 方法](https://docs.python.org/3/reference/datamodel.html?highlight=indices#slice.indices)。你可以先建立 `slice` 物件, 傳入序列的長度叫用 `indices()` 方法, 就會傳回一個序組, 告訴你起點、終點以及間距。例如以剛剛長度為 5 個字元的 "hello" 來說：

```python
>>> 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
```

## 負的間隔值會逆向取資料

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

```python
>>> '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()` 運算的三個參數](https://docs.python.org/3/library/functions.html#slice) 就可以知道會得到哪一段範圍的資料了。比如說：

```python
>>> 'hello'[slice(-1, 2, -1)]
'ol'
```

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

```python
>>> 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` 得到的結果是一樣的：

```python
>>> 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
          結              開
          尾              頭
                            <--負的間距
```

我們可以測試看看：

```python
>>> 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]` 取得反轉的序列：

```python
>>> 'hello'[::-1]
'olleh'
```

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

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

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

```python
>>> 'hello'[:2:-1]
'ol'
```

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

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

又例如：

```python
>>> 'hello'[2::-1]
'leh'
```

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

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

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