【Python】スライス操作についてまとめ


# 【Python】スライス操作についてまとめ

本記事は、Pythonのスライス操作についてまとめたものです。先行する記事との違いは以下です。

- 演算子の優先順位について説明
- 対話的環境で、スライスをインデクスに変換する方法を説明
- シーケンス内部で、スライスがどう処理されるか説明

私と同じ、Python初心者の役に立てばよいなと思います。

## 基本

シーケンス（リスト、文字列、タプル、バイト列）の一部分を切り取ってコピーを返してくれる仕組みを、**スライス**と呼びます。

通常、シーケンスへのアクセスは、以下のようにインデクスを指定して、その要素を取得する形で行います。

```python
sequence[index]
```

なお、シーケンスの先頭要素のインデクスは、1でなく、0です。


```python
s = 'Python'
s[3]
```




    'h'



他方、スライスでは、以下のように、始点と終点のインデクスを、コロンを挟む形で指定します。

```python
sequence[start:stop]
```

取得できるのは、要素ではなく、シーケンス（要素の列）となります。


```python
s = 'Python'
s[2:5]
```




    'tho'



今の例で、終点、すなわち、インデクスが5の要素（'n'）が含まれないことには、注意が必要です。[Pythonチュートリアル](https://docs.python.jp/3/tutorial/introduction.html#strings)に記載のとおり、以下のように考えるとよいです。

- インデクスは文字と文字の間(between)を指しており、最初の文字の左端が0になっている

![figure0000](https://github.com/tanuk1647/enjoy-slice-life/raw/master/figures/enjoy-slice-life-0000.png)


## 始点、終点の省略

始点、終点は省略可能です。始点省略時は、シーケンスの先頭から取得してくれます。終点省略時は、シーケンスの末尾まで取得してくれます。


```python
s = 'Python'
s[:5]
```




    'Pytho'




```python
s = 'Python'
s[2:]
```




    'thon'




```python
s = 'Python'
s[:]
```




    'Python'



最後の例は、ミュータブル（＝変更可能）なシーケンス——要するにリスト——のコピーを取る簡潔な方法として好まれます。

以下の例は、スライス操作によって別オブジェクトが生成されることを示しています。`id()`は、オブジェクトの一意なIDを取得する、組み込み関数です。


```python
s1 = [0, 1, 2, 3, 4, 5]
s2 = s1[:]
print(id(s1), id(s2))
```

    4366252232 4366252168


## 負数の指定

インデクスには、負数を指定することもできます。負数を指定した場合、末尾の要素を-1とする相対インデクスとして解釈されます。

![figure0001](https://github.com/tanuk1647/enjoy-slice-life/raw/master/figures/enjoy-slice-life-0001.png)


以下の例は、シーケンス末尾の2つの要素を取得する例です。


```python
s = 'Python'
s[-2:]
```




    'on'



## IndexError

スライス指定時は、インデクス指定時と異なり、IndexErrorが発生**しない**です。


```python
# インデクス指定時
import sys
s = 'Python'
try:
    s[100]
except:
    for info in sys.exc_info():
        print(info)
```

    <class 'IndexError'>
    string index out of range
    <traceback object at 0x1044a3888>



```python
# スライス指定時
s = 'Python'
s[0:100]
```




    'Python'



これは、シーケンスが、スライスをインデクスに変換するとき、**いい感じに**変換してくれるためです。

## 優先順位

[言語リファレンス](https://docs.python.jp/3/reference/expressions.html#operator-precedence)より、Pythonにおける演算子の優先順位を要約した表を引用します。最上段が、優先順位の最も低い (結合が最も弱い) もの。最下段が、最も高い (結合が最も強い) ものです。同じボックス内の演算子の優先順位は同じです。

|演算子|説明|
|:--|:--|
|lambda|ラムダ式|
|if – else|条件式|
|or|ブール演算 OR|
|and|ブール演算 AND|
|not x|ブール演算 NOT|
|in, not in, is, is not, <, <=, >, >=, !=, ==|帰属や同一性のテストを含む比較|
|&#124;|ビット単位 OR|
|^|ビット単位 XOR|
|&|ビット単位 AND|
|<<, >>|シフト演算|
|+, -|加算および減算
|\*, @, /, //, %|乗算, 行列積, 除算, 商, 剰余
|+x, -x, ~x|正数、負数、ビット単位 NOT|
|\*\*|べき乗|
|await x|Await 式|
|x\[index\], x\[index:index\], x(arguments...), x.attribute|添字指定、スライス操作、呼び出し、属性参照|
|(expressions...), \[expressions...\], {key: value...}, {expressions...}|式結合またはタプル表示、リスト表示、辞書表示、集合表示|

スライス操作は下から二段目に位置しており、優先順位が高いです。以下の例では、スライス操作が先に評価され、次に加算が評価されます。すなわち、'Pyt'という文字列と、'hon'という文字列の結合が行われます。


```python
s1 = 'Python'
s2 = s1[:3] + s1[3:]
s2
```




    'Python'



ただし、先ほどの表からは読み取れないのですが——コロンそのものの優先順位は低いです。

以下の例は、「次の数値は何か？」をディープラーニングさせるため、系列データに対して正解ラベルをセットしている例です。


```python
raw_data = [x for x in range(100)]
data_length = 9
data, label = [], []

for i in range(len(raw_data) - data_length):
    data.append(raw_data[i:i + data_length])  # ★
    label.append(raw_data[i + data_length])

print(data[0], label[0])
print(data[1], label[1])
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8] 9
    [1, 2, 3, 4, 5, 6, 7, 8, 9] 10


この例は本記事のために書き下ろしたものですが、qiitaのとある記事に掲載されていたコードを元にしたものです。

演算子の優先順位を知らずに、★の行を見ると、何のことだかわからないと思います。コロンの優先順位はプラスよりも低いため、あえてカッコを付けてコードの意図を明示するなら、以下のようになります。

```python
for i in range(len(raw_data) - data_length):
    data.append(raw_data[i:(i + data_length)])  # ★
    label.append(raw_data[i + data_length])
```

何のことはない、ただのスライス操作をしているだけだとわかります。

## ステップの指定

スライスでは、以下のように、ステップを指定することも可能です。

```python
sequence[start:stop:step]
```

ステップの省略時は、1を指定したものと見なされます。


```python
s = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even = s[::2]  # 偶数
odd = s[1::2]  # 奇数
print(even)
print(odd)
```

    [0, 2, 4, 6, 8, 10]
    [1, 3, 5, 7, 9]


ステップには負数を指定することも可能です。この場合、マイナス方向に（末尾から先頭に向かって）切り取りが行われます。以下の例は、シーケンスをリバースする簡潔な方法として好まれます。


```python
s = 'Python'
s[::-1]
```




    'nohtyP'



## スライスはどのようにインデクスに変換されるか

スライス指定時、内部的にはインデクスに変換され、インデクスで処理が行われます。今の例で、スライスはどのようにインデクスに変換されるでしょうか。実は、対話的環境で、以下のコードを実行すれば、簡単に確認することができます。


```python
s = 'Python'
slice(None, None, -1).indices(len(s))
```




    (5, -1, -1)



`slice()`は、[スライスオブジェクト](https://docs.python.jp/3/reference/datamodel.html#index-64)を生成します。そして、@shiracamus氏にコメントで補足説明して頂いたように、シーケンスに渡るのも、スライスオブジェクトです。引数は、先ほどの例で言えば、始点、終点、ステップの3つのうち、ステップのみが指定されていたので、`slice(None, None, -1)`となります。

indicesは、indexの複数形を表す英語で、インディスィーズと発音されます。`indices()`は、スライスオブジェクトとシーケンスの長さを元に、スライスをインデクスに変換してくれます。
今の例では、n（5）からP（-1）までマイナス方向に1つずつ（-1）切り取っていく——すなわちリバースする——ということだとわかります。

![figure0002](https://github.com/tanuk1647/enjoy-slice-life/raw/master/figures/enjoy-slice-life-0002.png)

`indices()`は、**いい感じ**にインデクスに変換してくれます。もし、大きすぎる数を指定しても、問題なく処理してくれます。


```python
s = 'Python'
slice(-100, 100, 1).indices(len(s))
```




    (0, 6, 1)



シーケンス（リスト、文字列、タプル、バイト列）内部でも、スライスがいい感じにインデクスに変換されるので、IndexErrorが発生しないのです。

## シーケンス内部で、スライスがどう処理されるか


リストについて、[CPythonの実装](https://github.com/python/cpython/blob/master/Objects/listobject.c)を見る限り、`indices()`に相当するCの関数ではなく、別のCの関数を2つ呼び出して、スライスを処理している様子です。理由はわかりません。Pythonに移植してみましたが、`indices()`と同じ動きをするように見えます。


```python
import sys

def pyslice_unpack(r):
    if r.step is None:
        step = 1
    else:
        step = r.step
        if step == 0:
            raise ValueError('slice step cannot be zero')
        if step < -sys.maxsize:
            step = -sys.maxsize
            
    if r.start is None:
        start = sys.maxsize if step < 0 else 0
    else:
        start = r.start
    
    if r.stop is None:
        stop = -sys.maxsize if step < 0 else sys.maxsize
    else:
        stop = r.stop
    return start, stop, step

def adjust_indices(length, start, stop, step):
    assert step != 0
    assert step >= -sys.maxsize
    
    if start < 0:
        start += length
        if start < 0:
            start = -1 if step < 0 else 0
    elif start >= length:
        start = length - 1 if step < 0 else length

    if stop < 0:
        stop += length
        if stop < 0:
            stop = -1 if step < 0 else 0
    elif stop >= length:
        stop = length - 1 if step < 0 else length
    
    return start, stop, step

def slice_indices(slice_, length):
    start, stop, step = pyslice_unpack(slice_)
    start, stop, step = adjust_indices(length, start, stop, step)
    return start, stop, step

print(slice_indices(slice(2, 5), 10))
print(slice_indices(slice(None, 5), 10))
print(slice_indices(slice(2, None), 10))
print(slice_indices(slice(None, None), 10))
print(slice_indices(slice(-2, None), 10))
print(slice_indices(slice(0, 100), 10))
print(slice_indices(slice(None, None, 2), 10))
print(slice_indices(slice(1, None, 2), 10))
print(slice_indices(slice(None, None, -1), 10))
print(slice_indices(slice(-100, 100, 1), 10))
```

    (2, 5, 1)
    (0, 5, 1)
    (2, 10, 1)
    (0, 10, 1)
    (8, 10, 1)
    (0, 10, 1)
    (0, 10, 2)
    (1, 10, 2)
    (9, -1, -1)
    (0, 10, 1)


ともあれ、もしシーケンスをユーザ定義することがあれば、スライスの処理には、`indices()`を活用できると思います。実際、以下の記事で、rangeを再実装した際にも活用しました。ご興味があれば、ご参照ください。

- [【Python】rangeを再実装し、計算量について学ぶ](https://qiita.com/tanuk1647/items/6051599e8eeff510a453)


## まとめ

スライス操作は便利なものですので、ぜひ、活用していきましょう！！