リストの2番目から4番目だけがほしい。ファイル名から拡張子を落としたい。学習を進めていると、こうした切り出しの場面が急に増えてきます。
そのたびにforループとifを書いていませんか。実はPythonには、こういう処理を1行で終わらせる書き方が用意されています。
それがスライスです。角かっこの中にコロンを書くだけの、とても短い記法です。
ただし、なぜ末尾が含まれないのか。マイナスの数字は何を意味するのか。ここが曖昧なままだと、1つずれた結果に悩まされ続けることになります。
この記事では、スライスの考え方と実務での使いどころを、手元で動かせるコードと一緒に整理していきます。
スライスは、切り出しのための書き方¶
まずは形から覚えましょう。基本の形は角かっこの中にコロンを1つ置くだけです。
コロンの左が開始位置、右が終了位置を表します。次のコードを写して動かしてみてください。
word = "PythonWebAcademy"
print(word[0:6]) # Python
print(word[6:9]) # Web
numbers = [10, 20, 30, 40, 50]
print(numbers[1:4]) # [20, 30, 40]
numbers[1:4] の結果に注目してください。1番目から4番目まで、と読むと4つ取れそうですが、実際には3つしか返ってきません。
Pythonの添字は0から始まり、そして終了位置の要素は結果に含まれません。ここがスライスで最初につまずく場所です。
添字の数え方そのものに自信がない方は、リストの基本から確認しておくと理解が速くなります。【関連記事】Pythonのリスト(list)とタプル(tuple)、どっちを使う? それぞれの違いを徹底解説
添字は文字と文字のあいだにあると考える¶
終了位置が含まれないのは不親切に見えるかもしれません。でも、ある見方をすると急に納得できます。
添字は要素そのものではなく、要素と要素のあいだの切れ目を指している。そう考えてみてください。
P y t h o n W e b
0 1 2 3 4 5 6 7 8 9
この図で word[0:6] は、0番の切れ目から6番の切れ目までを切り取る操作になります。切れ目の位置を指定していると考えれば、6番の文字が入らないのは当たり前です。
この考え方には、うれしい副作用があります。同じ位置で切ったスライスをつなぐと、必ず元に戻るのです。
word = "PythonWebAcademy"
print(word[:6] + word[6:] == word) # True
切れ目で分けているのだから、足せば元通り。境界での重複も抜けも起きません。
私は10年ほどエンジニアとして開発に関わってきましたが、この見方を知るまでは範囲の指定で毎回1つずれた結果に悩んでいました。切れ目を指定していると考えるようになってから、境界の計算で迷うことがほとんどなくなりました。
省略すれば、書く量はもっと減る¶
開始位置と終了位置は、どちらも省略できます。省略したときは、それぞれ先頭と末尾が使われます。
実務で書くスライスの多くは、この省略形です。
word = "PythonWebAcademy"
print(word[:6]) # Python 先頭から6番の切れ目まで
print(word[6:]) # WebAcademy 6番の切れ目から最後まで
print(word[:]) # 全部
3つめの word[:] は一見むだに見えますが、あとで説明するコピーの場面で活躍します。
末尾から数えるときはマイナスを使う¶
要素数がわからないデータを扱うこともあります。そんなときに便利なのが、マイナスの添字です。
マイナスは末尾から数えた位置を表します。 -1が最後の要素、-2がその1つ前です。
numbers = [10, 20, 30, 40, 50]
print(numbers[-2:]) # [40, 50] 最後の2件
print(numbers[:-1]) # [10, 20, 30, 40] 最後の1件を除く
print(numbers[-3:-1]) # [30, 40]
len() を使って numbers[len(numbers)-2:] と書く必要はありません。マイナスを使えば、要素数を知らなくても末尾から取り出せます。
ログの直近5件だけを見たい、といった場面でそのまま使える書き方です。
3つめの数字で、飛ばしながら取り出す¶
コロンをもう1つ足すと、3つめの数字が書けます。これは何個おきに取るかを表すもので、stepと呼ばれます。
省略したときの値は1です。つまり普段は1つずつ順番に取っている、ということになります。
numbers = [10, 20, 30, 40, 50]
print(numbers[::2]) # [10, 30, 50] 1つ飛ばし
print(numbers[1::2]) # [20, 40] 2番目から1つ飛ばし
ここまでの書き方を表にまとめておきます。迷ったときはここに戻ってきてください。
| 書き方 | 意味 | [10, 20, 30, 40, 50] での結果 |
|---|---|---|
x[1:4] |
1番の切れ目から4番の切れ目まで | [20, 30, 40] |
x[:3] |
先頭から3番の切れ目まで | [10, 20, 30] |
x[2:] |
2番の切れ目から最後まで | [30, 40, 50] |
x[-2:] |
末尾から2件 | [40, 50] |
x[::2] |
1つ飛ばしで全部 | [10, 30, 50] |
x[::-1] |
逆順 | [50, 40, 30, 20, 10] |
一定の間隔で並んだ数値がほしいだけなら、range関数のほうが向いている場面もあります。【関連記事】pythonのrange関数の使い方は?ステップ指定と逆順ループの小技をご紹介!
逆順にできるけれど、万能ではない¶
stepにマイナスを指定すると、後ろから順に取り出します。[::-1] は逆順にする書き方として、Pythonでは定番の表現です。
短くて便利なのですが、文字列に対して使うときは少し注意が必要です。
print("abc"[::-1]) # cba
print("käse"[::-1]) # es̈ak ←濁点にあたる記号がずれる
2行目の結果を見てください。eの上に付いていた記号が、sの上に移ってしまいました。
Pythonの文字列はコードポイントという単位の並びであり、見た目の1文字と一致しない場合があるためです。日本語の絵文字や、記号を組み合わせた文字でも同じことが起きます。
英数字や日本語のふつうの文章なら問題ありません。ただし、ユーザーが自由に入力した文字列を逆順にするなら、この挙動を頭の片隅に置いておいてください。
範囲を外れてもエラーにならない¶
ここがスライスのいちばんありがたい性質かもしれません。存在しない位置を指定しても、エラーになりません。
Python公式のチュートリアルでも、大きすぎる添字は長さに置きかえられ、開始位置が終了位置より後ろなら空になる、と説明されています。
numbers = [10, 20, 30, 40, 50]
print(numbers[2:100]) # [30, 40, 50] あるぶんだけ返る
print(numbers[10:20]) # [] 空のリスト
print(numbers[4:2]) # [] 逆転していても空
numbers[10] # IndexError: list index out of range
最後の行だけがエラーになる点が重要です。1つの要素を取り出す書き方は範囲外でエラーになりますが、スライスは静かに空を返します。
この性質のおかげで、件数を確認してから切り出す、といった前処理が不要になります。10件しかないデータに対して先頭20件を要求しても、10件が返ってくるだけです。
ただし、静かなことが裏目に出る場面もあります。私はデータの取得件数がゼロだったことにその場で気づけず、集計結果が空になった原因を調べるのに半日かけたことがありました。
空でも止まらないのだから、件数が意味を持つ処理では自分で確認する。そう決めておくと事故が減ります。
リストならスライスに代入できる¶
スライスは取り出すだけのものではありません。リストの場合は、切り出した範囲そのものを置きかえられます。
しかも、置きかえる前後で件数が違っても構いません。
nums = [10, 20, 30, 40, 50]
nums[1:3] = [99]
print(nums) # [10, 99, 40, 50] 2件が1件に置きかわる
nums2 = [10, 20, 30, 40, 50]
del nums2[1:3]
print(nums2) # [10, 40, 50] まとめて削除できる
del と組み合わせれば、範囲をまとめて消せます。ループを回して1件ずつ削除する必要はありません。
なお、ループの途中でリストから要素を削除すると、添字がずれて処理が飛ぶという有名な落とし穴があります。条件で絞りこむなら、内包表記で新しいリストを作るほうが安全です。【関連記事】Pythonのリスト内包表記を使いこなせ!3行のループを1行にまとめる書き方
スライスはコピーを作る、ただし浅いコピー¶
スライスの結果は、元のデータとは別の新しい入れ物です。ここを理解しておくと、思わぬ書き換え事故を防げます。
だから list_a[:] はリストのコピーを作る書き方として使われます。
a = [1, 2, 3]
b = a[:]
b.append(4)
print(a, b) # [1, 2, 3] [1, 2, 3, 4] aは無事
ところが、これで安心とは限りません。中身がリストや辞書だった場合は、入れ物だけが新しくなり、中の要素は元と同じものを指したままになります。
m = [[1, 2], [3, 4]]
n = m[:]
n[0].append(99)
print(m) # [[1, 2, 99], [3, 4]] ←元まで変わる
これが浅いコピーと呼ばれる状態です。入れ子になったデータをスライスでコピーしても、内側までは守られません。
内側まで独立させたいときは copy モジュールの deepcopy を使います。仕組みの詳しい説明はこちらにまとめてあります。【関連記事】Pythonの浅いコピー(シャローコピー)と深いコピー(ディープコピー)の違いを徹底解説!
もうひとつ、コピーである以上、切り出したぶんのメモリを新しく使います。100万件のリストを丸ごとスライスすれば、100万件ぶんの領域がもう1つできるということです。
スライスが使えない相手もいる¶
便利なスライスですが、どんなデータにも使えるわけではありません。順番を持った並びであることが条件です。
集合や辞書には使えません。ジェネレータのように、必要になるまで値を作らない相手にも使えないのです。
def gen():
for i in range(100):
yield i
next(gen()) # ここまでは動く
gen()[0:3] # TypeError: 'generator' object is not subscriptable
ジェネレータから先頭の数件だけがほしいときは、itertoolsのislice関数を使います。全体をリストに展開しないので、大きなデータでもメモリを節約できます。
import itertools
print(list(itertools.islice(gen(), 3))) # [0, 1, 2]
itertoolsには、こうした繰り返し処理を短く書く道具がそろっています。【関連記事】itertoolsを使いこなせ!複雑なループ処理を1行で美しく書くテクニック
また、文字列やタプルは中身を変えられないため、スライスへの代入もできません。"abc"[0:1] = "z" と書くとTypeErrorで止まります。
切り出す位置に名前を付けておく¶
最後に、実務で効いてくる小技をひとつ紹介します。スライスの指定は、slice関数で名前を付けて持ち回れます。
固定長のデータを扱うとき、角かっこの中に数字が並ぶと読み手が意味を追えなくなります。
YEAR = slice(0, 4)
MONTH = slice(5, 7)
row = "2026-08-10"
print(row[YEAR]) # 2026
print(row[MONTH]) # 08
row[0:4] と書くより、row[YEAR] のほうが意図が伝わります。切り出し位置が変わったときも、定義した1か所を直すだけで済みます。
固定長のテキストを読む処理では、この書き方に何度も助けられました。数字の意味を思い出す時間がなくなるからです。
つまずきやすいポイントを整理しておく¶
ここまでの内容を、実際に書くときに気をつける点だけに絞ってまとめます。
| つまずきポイント | 起きること | 対処 |
|---|---|---|
x[1:4] で4件取れると思う |
3件しか返らない | 終了位置は含まれないと覚える |
| 範囲外を指定した | エラーにならず空が返る | 件数が重要なら自分で確認する |
x[:] でコピーしたつもり |
入れ子の中身は共有される | copy.deepcopy を使う |
| ジェネレータをスライスする | TypeErrorで止まる | itertools.islice を使う |
文字列を [::-1] で逆順にする |
記号を組み合わせた文字が崩れる | 対象が英数字か日本語かを確認する |
覚えることは、そう多くありません。終了位置は含まれない、範囲外でも止まらない、コピーは浅い。 この3つを押さえておけば十分です。
まずは手元のリストに [-3:] と書いて、最後の3件を取り出すところから試してみてください。ループを書かずに済む手軽さを体感できれば、使う場面は自然と見えてきます。
ここまでお読みいただきありがとうございました。