【Python】スライス操作についてまとめ
本記事は、Pythonのスライス操作についてまとめたものです。先行する記事との違いは以下です。
- 演算子の優先順位について説明
- 対話的環境で、スライスをインデクスに変換する方法を説明
- シーケンス内部で、スライスがどう処理されるか説明
私と同じ、Python初心者の役に立てばよいなと思います。
基本
シーケンス(リスト、文字列、タプル、バイト列)の一部分を切り取ってコピーを返してくれる仕組みを、スライスと呼びます。
通常、シーケンスへのアクセスは、以下のようにインデクスを指定して、その要素を取得する形で行います。
sequence[index]
なお、シーケンスの先頭要素のインデクスは、1でなく、0です。
s = 'Python'
s[3]
'h'
他方、スライスでは、以下のように、始点と終点のインデクスを、コロンを挟む形で指定します。
sequence[start:stop]
取得できるのは、要素ではなく、シーケンス(要素の列)となります。
s = 'Python'
s[2:5]
'tho'
今の例で、終点、すなわち、インデクスが5の要素('n')が含まれないことには、注意が必要です。Pythonチュートリアルに記載のとおり、以下のように考えるとよいです。
- インデクスは文字と文字の間(between)を指しており、最初の文字の左端が0になっている
始点、終点の省略
始点、終点は省略可能です。始点省略時は、シーケンスの先頭から取得してくれます。終点省略時は、シーケンスの末尾まで取得してくれます。
s = 'Python'
s[:5]
'Pytho'
s = 'Python'
s[2:]
'thon'
s = 'Python'
s[:]
'Python'
最後の例は、ミュータブル(=変更可能)なシーケンス——要するにリスト——のコピーを取る簡潔な方法として好まれます。
以下の例は、スライス操作によって別オブジェクトが生成されることを示しています。id()
は、オブジェクトの一意なIDを取得する、組み込み関数です。
s1 = [0, 1, 2, 3, 4, 5]
s2 = s1[:]
print(id(s1), id(s2))
4366252232 4366252168
負数の指定
インデクスには、負数を指定することもできます。負数を指定した場合、末尾の要素を-1とする相対インデクスとして解釈されます。
以下の例は、シーケンス末尾の2つの要素を取得する例です。
s = 'Python'
s[-2:]
'on'
IndexError
スライス指定時は、インデクス指定時と異なり、IndexErrorが発生しないです。
# インデクス指定時
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>
# スライス指定時
s = 'Python'
s[0:100]
'Python'
これは、シーケンスが、スライスをインデクスに変換するとき、いい感じに変換してくれるためです。
優先順位
言語リファレンスより、Pythonにおける演算子の優先順位を要約した表を引用します。最上段が、優先順位の最も低い (結合が最も弱い) もの。最下段が、最も高い (結合が最も強い) ものです。同じボックス内の演算子の優先順位は同じです。
演算子 | 説明 |
---|---|
lambda | ラムダ式 |
if – else | 条件式 |
or | ブール演算 OR |
and | ブール演算 AND |
not x | ブール演算 NOT |
in, not in, is, is not, <, <=, >, >=, !=, == | 帰属や同一性のテストを含む比較 |
| | ビット単位 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'という文字列の結合が行われます。
s1 = 'Python'
s2 = s1[:3] + s1[3:]
s2
'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のとある記事に掲載されていたコードを元にしたものです。
演算子の優先順位を知らずに、★の行を見ると、何のことだかわからないと思います。コロンの優先順位はプラスよりも低いため、あえてカッコを付けてコードの意図を明示するなら、以下のようになります。
for i in range(len(raw_data) - data_length):
data.append(raw_data[i:(i + data_length)]) # ★
label.append(raw_data[i + data_length])
何のことはない、ただのスライス操作をしているだけだとわかります。
ステップの指定
スライスでは、以下のように、ステップを指定することも可能です。
sequence[start:stop:step]
ステップの省略時は、1を指定したものと見なされます。
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]
ステップには負数を指定することも可能です。この場合、マイナス方向に(末尾から先頭に向かって)切り取りが行われます。以下の例は、シーケンスをリバースする簡潔な方法として好まれます。
s = 'Python'
s[::-1]
'nohtyP'
スライスはどのようにインデクスに変換されるか
スライス指定時、内部的にはインデクスに変換され、インデクスで処理が行われます。今の例で、スライスはどのようにインデクスに変換されるでしょうか。実は、対話的環境で、以下のコードを実行すれば、簡単に確認することができます。
s = 'Python'
slice(None, None, -1).indices(len(s))
(5, -1, -1)
slice()
は、スライスオブジェクトを生成します。そして、@shiracamus氏にコメントで補足説明して頂いたように、シーケンスに渡るのも、スライスオブジェクトです。引数は、先ほどの例で言えば、始点、終点、ステップの3つのうち、ステップのみが指定されていたので、slice(None, None, -1)
となります。
indicesは、indexの複数形を表す英語で、インディスィーズと発音されます。indices()
は、スライスオブジェクトとシーケンスの長さを元に、スライスをインデクスに変換してくれます。
今の例では、n(5)からP(-1)までマイナス方向に1つずつ(-1)切り取っていく——すなわちリバースする——ということだとわかります。
indices()
は、いい感じにインデクスに変換してくれます。もし、大きすぎる数を指定しても、問題なく処理してくれます。
s = 'Python'
slice(-100, 100, 1).indices(len(s))
(0, 6, 1)
シーケンス(リスト、文字列、タプル、バイト列)内部でも、スライスがいい感じにインデクスに変換されるので、IndexErrorが発生しないのです。
シーケンス内部で、スライスがどう処理されるか
リストについて、CPythonの実装を見る限り、indices()
に相当するCの関数ではなく、別のCの関数を2つ呼び出して、スライスを処理している様子です。理由はわかりません。Pythonに移植してみましたが、indices()
と同じ動きをするように見えます。
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を再実装した際にも活用しました。ご興味があれば、ご参照ください。
まとめ
スライス操作は便利なものですので、ぜひ、活用していきましょう!!