はじめに
Pythonのスライスではどんなに大きい/小さい数を指定してもインデックスエラーが起きないというのは有名な話(筆者はしばしば忘れる)ですが、その理由について「内部でいい感じに処理してくれるから」以上の説明をしてくれている日本語の記事が見つからないなと思ったので、隙間産業として執筆しようと思う次第であります。
本記事では、掲題のテーマについて思想と実装の2つの観点から説明しようと思います。
Pythonを始めたてで、「特定の要素を抽出する時はインデックスエラー五月蝿く言われるのにスライスだと言われないのなんかキモい」と感じる人のモヤモヤが少しでも解消できればと思います。
また筆者自身も経験が浅いので、表現が正確でないところがございましたら、後学のためにもご指摘いただけると幸いです。
(そもそも)スライスとは何をするものなのか
本項はスライスの思想部分に当たります。
公式ドキュメントでもあまり「スライスとは何か」を単純明快に書いていない(ように見える)のでもどかしいところではあるのですが、一応以下のような説明があります。
-
s[i:j]
についての注
s の i から j へのスライスは i <= k < j となるようなインデックス k を持つ要素からなるシーケンスとして定義されます。
-
s[i:j:k]
についての注
s の「 i から j まででステップが k のスライス」は、インデックス x = i + n*k (ただし n は 0 <= n < (j-i)/k を満たす任意の整数)を持つ要素からなるシーケンスとして定義されます。
ちゃんと読めば理解できるけどちょっとめんどくさいって感じですよね。
いま必要なところだけ抽出すると、スライスとは「元のシーケンスから指定したインデックスに該当する要素からなる新しいシーケンスを作る」操作であるということになるでしょうか。
(シーケンスとはリスト、タプル、range、文字列、バイト列などのデータ型を指します。)
これがなぜ大事かということについて、Stack Overflowから引用します。
Indexing returns a single item, but slicing returns a subsequence of items. So when you try to index a nonexistent value, there's nothing to return. But when you slice a sequence outside of bounds, you can still return an empty sequence.
意訳すると、
- 要素抽出では単一の要素を返すけどスライスはシーケンスを返す。
- 要素抽出で存在しないインデックスを指定すると返すものがなくなってしまうがシーケンスなら空のシーケンスを返せる。
ということを言っています。1については上で触れたので説明は不要かと思いますが、2はどうでしょうか。
要するに、[0,1,2][3]
は返り値がないためエラーになるが、[0,1,2][3:]
はインデックス3以上に該当する要素がない場合空のリスト[]
を返り値とすることができるのでエラーにしなくて良いということです。
そうは言っても、これを読んでいるアントワネットな方々(筆者を含む)は「返り値がないといけないなら[0,1,2][3]
でNoneが返るようにすればいいじゃない」とお思いになるのではないでしょうか。
しかしそれだと**[0,1,2][3]
でNoneが返っているのか[0,1,2,None][3]
でNoneが返っているのか**判別が難しくなってしまうので、やはりインデックスエラーは必要なのです(と先の引用の続きで親切に補足してくれていました)。
些か説明が冗長だったかもしれませんが、結論としては「スライスは該当要素が一つもなくても空のシーケンスを返せるため」エラーが発生しないということでした。
(蛇足:「部分集合/部分文字列」とか「空集合」のような用語をついつい使いたくなってしまいますが、「集合」といってしまうとシーケンス型の「順序」という要素を無視することになってしまうので、結局公式ドキュメントのように説明するしかないんですよね。。)
「いい感じ」の正体
本項は、スライスの実装部分に当たります。
[P,y,t,h,o,n][100:200]
のような操作をどうやって「いい感じ」に処理しているのか、その答えは前項の公式ドキュメントからの引用の続きの部分に隠されているのです。
-
s[i:j]
についての注
i または j が len(s) よりも大きい場合、 len(s) を使います。 i が省略されるか None だった場合、 0 を使います。 j が省略されるか None だった場合、 len(s) を使います。 i が j 以上の場合、スライスは空のシーケンスになります。
つまり[P,y,t,h,o,n][100:200]
では、i
、j
がどちらもlen(s)
より大きいのでlen(s)
が用いられ、i (=len(s)) >= j (=len(s))
が成立するので空のシーケンスを返すと判断しているわけです。
スライスも内部的にはIndexingを行っているので、len(s)
より大きい数は予め変換しておくわけですね。step
を指定する場合も基本的には同じような処理をしています。
最後に、この辺りの処理がCPythonでどのように実装されているか、参考として載せておきます。筆者もCに強いわけではないので、「あー確かにそれっぽいこと書かれてるなー」程度に見ていただければよいかと思います(ソースコードにも/* this is harder to get right than you might think */
と書かれていますし)。
(本項ではstep
を使用しない場合の説明をしましたが、引用コードではstep
を使用する場合の処理になっています。あしからず)
defstop = *step < 0 ? -1 : length;
...
if (r->stop == Py_None) {
*stop = defstop;
}
...
if ((*step < 0 && *stop >= *start)
|| (*step > 0 && *start >= *stop)) {
*slicelength = 0;
cpython: 3a1db0d2747e Objects/sliceobject.c
結論
スライスは便利。
最後までご覧頂きありがとうございました。
参考サイト
組み込み型 — Python 3.8.5 ドキュメント
【Python】スライス操作についてまとめ - Qiita
python - Why does substring slicing with index out of range work? - Stack Overflow
string - Why python's list slicing doesn't produce index out of bound error? - Stack Overflow