__getitem__
とは?
Pythonの特殊メソッドのひとつで、オブジェクトに角括弧でアクセスしたときの挙動を定義できる。
class Sample:
def __getitem__(self, item):
return item
a = Sample()
a["foo"] # => foo
a[1] # => 1
今回使ったPythonのバージョン
比較的昔からあるので、基本的な挙動は変わってないと思うけど。念の為。
$ python --version
Python 3.6.6
カンマ区切りアクセス
Python組み込みのリストなどではできないが、例えばnumpyでは、
import numpy as np
a = np.array([[1,2],[3,4]])
a[0,1] # => 2 (0行目、1列目の要素)
のようなアクセスができる。
このとき、__getitem__
にはどのような引数が来ているのかを調べた。
結果、カンマ区切りで複数指定したときには、タプルとして渡されていることが分かった。
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つだけのときはタプルではなく、複数のときはタプルだという。
うーん、何とも言えない微妙な感じ。
もう少し調べてみた。
# タプルを普通に入れても、タプルのタプルにはならない
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]
のような、スライス表記がよく使われる。
スライスについても調べてみた。
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型のオブジェクトは、どうやったら作ることができるのか。また、変数に入れることはできるのか。実際に試してみた。
# スライス表記で変数に代入することはできない
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]
ちょっとした遊び心から、クレイジーなスライスを作ってみた。
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
関数を使うと、どんなメソッドを持ってるのか分かる。
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つの値を取得するものっぽい。実際に試してみた。
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のソースを読んでみた。
結果、これらの比較は、両辺がslice
型であることを確認して、そうであった場合、左辺、右辺とも(start, stop, step)
のタプルを作って、タプル同士を比較していることがわかった。
ちなみに、タプルの比較は、最も左のものから順に比較するようになっている。
手元で試したら、確かにそんな感じの挙動になっていたが、全く使い道が思い浮かばないので、検証コードの記述は省略。
(使い道あるよ!って人は、1 < None
など、int
とNoneType
の比較ができないことに注意。これは罠!)
indicesメソッド
あと、indices
も気になった。とりあえずhelpを見る。
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
などでも、スライスで書いた場合は範囲外は無視されるみたい。知らなかった。
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にある。
int
型でしかインデックスを引けないことにしたらnp.int16
型などでインデックスが引けなくなり、かといって__int__
を持っているものにまで範囲を広げるとfloat
型を指定したときに、浮動小数点以下を切り捨てる形でインデックスが引けてしまうのが問題なので、インデックスを引ける整数型には__index__
メソッドを持たせた、ということらしい。
重箱の隅はこれくらいにして。挙動を見ていく。
詳細な挙動はcpythonのソースを参考にした。
大まかには分かったものの、細かい部分は、なんでそういう挙動なのか意義が分からない。
-
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__
メソッドに与える引数やスライスについて、細かい挙動を追ってみた。
とくにスライス周りで、よく分からない仕様はあるものの、実用上は困らなさそうな程度には理解できた。