静かなる名辞

pythonとプログラミングのこと


【python】sliceのちょっと深イイ(かもしれない)話

 リスト(じゃなくてもだけど)に次のようにアクセスするとき、内部的には__getitem__が呼ばれていることは、歴戦のpythonistaの皆さんには常識でしょう。

>>> lst = [1,2,3,4,5]
>>> lst[0]
1

 この様子を自作クラスで観察してみましょう。

>>> class Hoge:
...     def __getitem__(self, k):
...         print("__getitem__!")
...         return k
... 
>>> h = Hoge()
>>> h[0]
__getitem__!
0
>>> h["hogehoge~"]
__getitem__!
'hogehoge~'

 Hogeクラスは__getitem__が呼ばれると、「__getitem__!」とprintしてから__getitem__の引数をそのまま返します。上の結果から、実際に__getitem__が呼ばれていることがわかります。

 []は__getitem__の糖衣構文と言っても、まあ良いでしょう*1

 これで「ほーん、そうか」と納得しかけてしまいますが、「ちょっと待て、じゃあスライスはどうなるんだ・・・?」というのが疑問として浮かんできますね。

lst[0:5]

 一体何が渡るというんでしょう。「0:5」なんてオブジェクトはありませんから(そのまま書いても構文エラー)、スライスの場合は__getitem__とは違う仕組みで処理されるのでしょうか?

 そうはなっていません。

>>> h[0:5]
__getitem__!
slice(0, 5, None)
>>> type(h[0:5])
__getitem__!
<class 'slice'>

 おおお、sliceオブジェクトなんていうのが渡っている・・・。

 このsliceオブジェクトは、普通にpythonを書いている限り目にする機会はほとんどないと思います。

 それでも、公式ドキュメントにはしっかり載っています。

class slice(start, stop[, step])
range(start, stop, step) で指定されるインデクスの集合を表す、スライス (slice) オブジェクトを返します。引数 start および step はデフォルトでは None です。スライスオブジェクトは読み出し専用の属性 start、stop および step を持ち、これらは単に引数で使われた 値 (またはデフォルト値) を返します。これらの値には、その他のはっきりと した機能はありません。しかしながら、これらの値は Numerical Python および、その他のサードパーティによる拡張で利用されています。スライスオブジェクトは拡張されたインデクス指定構文が使われる際にも生成されます。例えば a[start:stop:step] や a[start:stop, i] です。この関数の代替となるイテレータを返す関数、itertools.islice() も参照してください。

2. 組み込み関数 — Python 3.6.5 ドキュメント

 なるほど~、こいつが渡ることで、あとは受けるオブジェクトの__getitem__が然るべき処理をしてくれればスライスが実現する仕組みになっているんですね。よくできてる・・・。

「いや、ちょっと待て。numpyで使うアレはどうなってるんだ」

 こういう奴のことですね。

a[:,0]

 上記sliceオブジェクトはstart, stop, stepしか持たないので、こういうスライスは手に負えなさそうです。

 なんてこった、今度こそ__getitem__では処理しきれなくて、なにか違う仕組みで処理されているのか・・・

 いません。

>>> h[:,0]
__getitem__!
(slice(None, None, None), 0)
>>> type(h[:,0])
__getitem__!
<class 'tuple'>

 __getitem__なのは間違いないみたいですが、 なぜかtupleが返ります。0要素目は空のslice, 二番目は入力がそのまま・・・? 一体どうなっているんだ?

 実はこうなっています。

>>> h[::,0:1:2,0:-1:-2,0,1,2,3]
__getitem__!
(slice(None, None, None), slice(0, 1, 2), slice(0, -1, -2), 0, 1, 2, 3)

 なるほど、カンマで区切られたものごとにtupleの要素になっているのか!

 ……と、書きましたが一応ちゃんと説明しておくと、そもそも「pythonのtuple」はカンマで区切られた要素によって成立する構文です。関数の引数リストなど、他の構文と被る場合はそちらが優先的に扱われますが。

>>> 1,2,3
(1, 2, 3)

 これについてはドキュメントに説明があります。

4. 組み込み型 — Python 3.6.5 ドキュメント


 要するに、単に添字の中にtupleを書いているだけ、とみなしてもまあ良いでしょう*2

 わかっちゃえばなんてことはないですね。とても自然な仕様です。あとは受け取った側で然るべき処理をするだけ。

 このように、スライスの裏ではsliceオブジェクトが暗躍しています。なんとなくこういうことを知っていると深イイと思えますね。また、なんとなくスライスの構文がよくわからなかった人も、この仕様が理解できれば自然に複雑なスライスを書けるようになることでしょう(その結果、難読コードが量産されてしまうかもしれないが・・・)。

余談

>>> lst = [0,1,2]
>>> lst[:,0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers or slices, not tuple

 上のことがわかっていれば理解できますが、そうでないと絶対理解不能なエラーメッセージです(tupleなんかないじゃんかって)。

追記:2018/06/21

 この話はドキュメントのどこに載っているんだろうとずっと思っていましたが、見つけました。

6. 式 (expression) — Python 3.6.5 ドキュメント

スライス表記に対する意味付けは、以下のようになります。プライマリの値評価結果は、以下に述べるようにしてスライスリストから生成されたキーによって (通常の添字表記と同じ __getitem__() メソッドを使って) インデクス指定できなければなりません。スライスリストに一つ以上のカンマが含まれている場合、キーは各スライス要素を値変換したものからなるタプルになります; それ以外の場合、単一のスライス要素自体を値変換したものがキーになります。一個の式であるスライス要素は、その式に変換されます。適切なスライスは、スライスオブジェクト (標準型の階層 参照) に変換され、その start, stop および step 属性は、それぞれ指定した下境界、上境界、およびとび幅 (stride) になります。式がない場所は None で置き換えられます。

 説明の内容はこの記事と基本的に同じですが、グダグダなこの記事と比べると、さすがによくまとまっています。公式は偉い。

*1:細かいことを言うと、lst[0] = "hoge"など代入する場合、del dct[0]などdel文を呼ぶ場合などは__getitem__とは異なったメソッドが呼ばれます。あくまでも単純に値を参照する場合の話です

*2:sliceオブジェクトに変換されるので単純な処理ではないが