Pythonの __getitem__ に与える引数やスライスについて調べてみた

# `__getitem__` とは?
Pythonの特殊メソッドのひとつで、オブジェクトに角括弧でアクセスしたときの挙動を定義できる。

```py:__getitem__の簡単な例
class Sample:
    def __getitem__(self, item):
        return item

a = Sample()
a["foo"] # => foo
a[1] # => 1
```

# 今回使ったPythonのバージョン
比較的昔からあるので、基本的な挙動は変わってないと思うけど。念の為。

```shell-session
$ python --version
Python 3.6.6
```

# カンマ区切りアクセス
Python組み込みのリストなどではできないが、例えばnumpyでは、

```py:カンマ区切りアクセスの例
import numpy as np
a = np.array([[1,2],[3,4]])
a[0,1] # => 2 (0行目、1列目の要素)
```

のようなアクセスができる。
このとき、`__getitem__` にはどのような引数が来ているのかを調べた。

結果、カンマ区切りで複数指定したときには、タプルとして渡されていることが分かった。

```py:カンマ区切りの調査
class Sample:
    def __getitem__(self, item):
        return item

a = Sample()
a[123] # => 123
type(a[123]) # => int

a[1,23] # => (1, 23)
type(a[1,23]) # => tuple

a[1,2,"3"] # => (1, 2, "3")
type(a[1,2,"3"]) # => tuple
```

1つだけのときはタプルではなく、複数のときはタプルだという。
うーん、何とも言えない微妙な感じ。

もう少し調べてみた。

```py:カンマ区切りのさらなる調査
# タプルを普通に入れても、タプルのタプルにはならない
a[(1,2,3)] # => (1,2,3)
a[(1,2,3)] == a[1,2,3] # => True

# なので、dict型でも、実はカンマ区切りとタプルは同じ扱い
d = {}
d[(1,2)] = 3
d[1,2] # => 3

# タプルをカンマ区切りで書くと、タプルのタプルになる
a[(1,2),3] # => ((1,2),3)

# 次のようにすれば、1要素のタプルを引数にできる
a[(1,)] # => (1,)
a[1,] # => (1,)

# 空タプルの指定。 a[] はSyntaxErrorになる
a[()] # => ()
a[] # => SyntaxError: invalid syntax

# 変数での指定も、これまでの流れをわかっていれば怖くない。
t = (3, 4)
d[t] = 5
d[3, 4] # => 5

# タプルのアスタリスクでの展開はできない
d[*t] # => SyntaxError: invalid syntax
# 1要素のタプルでももちろん同様
d[9] = 9
t = (9,)
d[t] # => KeyError: (9,)
d[*t] # => SyntaxError: invalid syntax
```

`__getitem__` のタプルのルール、完全に理解した。

# スライス
角括弧の中では `a[1:3]` のような、スライス表記がよく使われる。
スライスについても調べてみた。

```py:sliceオブジェクト
class Sample:
    def __getitem__(self, item):
        return item

a = Sample()
a[1:3] # => slice(1, 3, None)
type(a[1:3]) # => slice
a[:] # => slice(None, None, None)
a[1:2:3] # => slice(1, 2, 3)
a[1:2:3:4] # => SyntaxError: invalid syntax
a[1:,2:3:4,5,::6] # => (slice(1, None, None), slice(2, 3, 4), 5, slice(None, None, 6))
```

スライス表記は、slice型のオブジェクトとして渡される。
また、カンマ区切りでスライスと数字を混在させることもできた。

slice型のオブジェクトは、どうやったら作ることができるのか。また、変数に入れることはできるのか。実際に試してみた。

```py:sliceオブジェクト?
# スライス表記で変数に代入することはできない
r = 1:3 # => SyntaxError: invalid syntax
r = [1:3] # => SyntaxError: invalid syntax

# slice()を使って作成可能
r = slice(4) # => slice(None, 4, None)
r = slice(1,3) # => slice(1, 3, None)
r = slice() # => TypeError: slice expected at least 1 arguments, got 0
[0,1,2,3,4][slice(2)] # => [0,1]
[0,1,2,3,4][slice(1,3)] # => [1,2]
r = slice(2)
[0,1,2,3,4][r] # => [0,1]
```

ちょっとした遊び心から、クレイジーなスライスを作ってみた。

```py:こんなsliceアリなのか!?
class Sample:
    def __getitem__(self, item):
        return item

a = Sample()
# 長くなるから書いてないけど、slice()を使っても、これらは作れる。
a["aaa":"bcd"] # => slice('aaa', 'bcd', None)
a[None:1.23:"45"] # => slice(None, 1.23, '45')
a[slice(1,2,3):slice(4,5,6):slice([], (), {})] # => slice(slice(1, 2, 3), slice(4, 5, 6), slice([], (), {}))
```

意味不明だが。作れるには作れる。自作オブジェクトでも何でも入る。

# sliceオブジェクトでできること
`dir`関数を使うと、どんなメソッドを持ってるのか分かる。

```py:dir(slice(1))
dir(slice(1))
# => ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']
```

## start, stop, step
`start`, `step`, `stop`がある。いかにも、`slice`が持ってる3つの値を取得するものっぽい。実際に試してみた。

```py:start,stop,step
r = slice(1,2,3)
r.start # => 1
r.stop # => 2
r.step # => 3

# Noneでもエラーが出ないことを確認
r  = slice(1) # r: slice(None, 1, None)
r.start # => None
```

予想通り。

## 比較演算
さっき`dir`をしたとき、`==`とか`<`とかの比較演算をする特殊メソッドの`__eq__`とか`__lt__`とかがあったのが気になった。
`==`や`!=`はともかく、他は挙動がさっぱり分からなかったので、[cpythonのソース](https://github.com/python/cpython/blob/v3.6.6/Objects/sliceobject.c#L566)を読んでみた。
結果、これらの比較は、両辺が`slice`型であることを確認して、そうであった場合、左辺、右辺とも`(start, stop, step)`のタプルを作って、タプル同士を比較していることがわかった。
ちなみに、タプルの比較は、最も左のものから順に比較するようになっている。

手元で試したら、確かにそんな感じの挙動になっていたが、全く使い道が思い浮かばないので、検証コードの記述は省略。
(使い道あるよ!って人は、`1 < None`など、`int`と`NoneType`の比較ができないことに注意。これは罠!)

## indicesメソッド
あと、`indices`も気になった。とりあえずhelpを見る。

```py:help(slice.indices)
help(slice.indices)
# =>
indices(...)
    S.indices(len) -> (start, stop, stride)
    
    Assuming a sequence of length len, calculate the start and stop
    indices, and the stride length of the extended slice described by
    S. Out of bounds indices are clipped in a manner consistent with the
    handling of normal slices.
```

長さを引数として渡せば、範囲などを整えて返してくれるメソッドっぽい。Noneを埋めたり、インデックスにマイナスを指定したら「後ろから何要素目」が表せるが、それをプラスのインデックスに直したり、範囲外のインデックスを無視したり、など。
え、範囲外は無視? 実は`list`などでも、スライスで書いた場合は範囲外は無視されるみたい。知らなかった。

```py:スライスの範囲外は無視される
ls = [0,1,2]
ls[3] # => IndexError: list index out of range
ls[3:4] # => [] スライスだとIndexErrorが出ない
# ちなみに、stepが0はダメ。これはslice.indicesでも同じ。
ls[::0] # => ValueError: slice step cannot be zero
```

ちなみに、`str`とか`float`とかの入ったスライスで使うと`TypeError: slice indices must be integers or None or have an __index__ method`が出る。

`__index__` メソッドについては[PEP 357](https://docs.python.org/2.5/whatsnew/pep-357.html)にある。
`int`型でしかインデックスを引けないことにしたら`np.int16`型などでインデックスが引けなくなり、かといって`__int__`を持っているものにまで範囲を広げると`float`型を指定したときに、浮動小数点以下を切り捨てる形でインデックスが引けてしまうのが問題なので、インデックスを引ける整数型には`__index__`メソッドを持たせた、ということらしい。

重箱の隅はこれくらいにして。挙動を見ていく。
詳細な挙動は[cpythonのソース](https://github.com/python/cpython/blob/v3.6.6/Objects/sliceobject.c#L369)を参考にした。

大まかには分かったものの、細かい部分は、なんでそういう挙動なのか意義が分からない。

- `step` 
    - `0`なら、`ValueError`を出す
    - `None`なら、`step = 1`に修正する
- `lower`, `upper` (関数内で使う)
    - `step`が正の場合
        - `lower = 0`, `upper = length`
    - `step`が負の場合
        - `lower = -1`, `upper = length - 1`
- `start`
    - `None`なら
        - `step`が正なら`start = lower`, 負なら`start = upper`
    - 負なら
        - `start += length`
        - `start < lower` だった場合は `start = lower`
    - 0または正なら
        - `start > upper` だった場合は `start = upper`
- `stop`
    - `None`なら
        - `step`が正なら`stop = upper`, 負なら`stop = lower`
    - 負なら
        - `stop += length`
        - `stop < lower` だった場合は `stop = lower`
    - 0または正なら
        - `stop > upper` だった場合は `stop = upper`

`step`が負で、`start`や`stop`が`-length`よりも小さかったときの挙動が、謎で仕方ない。
わざとそうすること、あるのかなぁ?

# まとめ
`__getitem__`メソッドに与える引数やスライスについて、細かい挙動を追ってみた。
とくにスライス周りで、よく分からない仕様はあるものの、実用上は困らなさそうな程度には理解できた。