[Pythonチートシート]特殊メソッド編:Pythonチートシート
インスタンスの初期化など、オブジェクトの振る舞いをプログラマーが細かく調整するために使用できる特殊メソッドについてギュッとまとめた。
Pythonでは、クラスの定義時に、そのクラスのインスタンスの振る舞いを細かく調整するためにさまざまな「特殊メソッド」が提供されている。今回はこれらについて見ていこう。
特殊メソッドとは
Pythonのクラスでは「特殊メソッド」と呼ばれるメソッドを定義(オーバーライド)できる。特殊メソッドとは、各種の演算子や組み込み関数などの操作の対象として、独自のクラスを利用できるようにするための仕組みだと考えられる。つまり、クラスを自分で定義しているときに、適切な名前の特殊メソッドを適切にオーバーライドすることで、例えば、次のような処理が可能になる。
- インスタンスの生成と初期化
- インスタンス同士の比較
- 他の型への変換
- 数値として演算
- 反復可能オブジェクト(コンテナ)的な動作
- 関数的な動作
特殊メソッドの名前は、特定の処理を示す名前を、2つのアンダースコア「__」で囲んだものになる。例えば、「__init__」は「インスタンスの初期化」を意味する「init」を「__」で囲んだものになっている。
特殊メソッドは非常に多いので、以下ではその幾つかを抜粋して紹介していく。
インスタンスの生成と破壊
上でも軽く触れたように、クラスのインスタンス(オブジェクト)を初期化するための__init__メソッドは、特殊メソッドの代表的な例である。Pythonでは、インスタンスの生成や破壊に関する特殊メソッドとして次のようなものがある。
特殊メソッド | 対応する操作/演算 | 説明 |
---|---|---|
__new__(cls, ……) | クラスのインスタンス生成時 | クラスのインスタンスを生成するために呼び出される |
__init__(self, ……) | クラスのインスタンスの初期化時 | クラスのインスタンスの生成後に、それを初期化するた めに呼び出される |
__del__(self) | クラスのインスタンスの破壊時 | クラスのインスタンスが破壊されるときに呼び出される |
インスタンスの生成と破壊に関連する特殊メソッド |
__new__メソッドは、クラスのインスタンス生成をカスタマイズする際に定義する。暗黙の第1パラメーターには「self」ではなく「cls」を指定する。インスタンスの生成自体は、親クラスの__new__メソッドを呼び出して、cls(とその他の引数)を指定する、つまり「super().__new__(cls, ……)」とするのが典型的なやり方だ。加えて、__new__メソッドでしか行えないインスタンスの初期化も行える(後述)。
__init__メソッドは、既にご存じの通り、インスタンスの初期化を行うために使用する。__new__メソッドを定義した場合、そこで作成されたインスタンスは__init__メソッドへと引き渡される。
__del__メソッドは、インスタンスが破壊されるタイミングで自動的に呼び出される。特殊なリソースを破壊する必要があるようなときには、これを定義する必要があるだろう。注意点は、このメソッドが呼び出されるタイミングだ。つまり、「del インスタンス」を実行したタイミングで呼び出されるわけではない。インスタンスが破壊される(このメソッドが呼び出される)のは、それに結び付けられている名前がなくなった時点なので注意しよう。また、基底クラスで__del__メソッドが定義されているのであれば、それらを呼び出して、オブジェクトの破壊が確実に行われるようにする必要がある。
以下に例を示す。
class Foo:
def __new__(cls):
print('__new__')
self = super().__new__(cls) # インスタンス生成を行う典型的なコード
self.attr = 'set in __new__' # ここでしかできない初期化処理を書いてもよい
return self # 生成したインスタンスを返す
def __init__(self, name='foo'):
print('__init__')
self.name = name # インスタンスの初期化処理
def __del__(self):
#super().__del__() # 基底クラスに__del__メソッドがあれば必ず呼び出す
print('__del__') # インスタンスが破壊されるときに行う処理
foo = Foo() # '__new__'と'__init__'が表示される
print('foo.attr:', foo.attr) # 'foo.attr: set in __new__'
bar = foo
print('bar.name:', bar.name) # 'bar.name: foo'
print('del foo') # この時点ではまだ生成したインスタンスには別名がある
del foo
print('del bar')
del bar # '__del__':この時点でインスタンスを束縛する名前がなくなる
この例では、__new__メソッドで「super().__new__(cls)」によりインスタンスを作成した後、「attr」という名前の属性(インスタンス変数)を定義して、それを戻り値として返送している。__init__メソッドでは、これを受け取り、それに対して「name」という名前の属性を設定している。これにより、このインスタンスは2つの属性を持つことになる。
クラス定義後のコードでは、このクラスのインスタンスを生成して、それを2つの変数に代入している。そのため、「del foo」「del bar」の2つを実行してインスタンスに関連付けられた名前がなくなるまでは、__del__メソッドが呼び出されることはない。
__new__メソッドはインスタンス生成に、__init__メソッドはインスタンスの初期化に使うが、これらをまとめずに2つのメソッドとしている理由の一つとして、__init__メソッドでは「変更不可能なオブジェクトを初期化できない」ことが挙げられる。変更不可能なクラスのインスタンス生成処理においては、それが生成された時点で変更不可能なので、その初期化は__init__メソッドが呼び出されるよりも前に行う必要がある。つまり、そうしたインスタンスの生成と初期化は__new__メソッドで行う。
以下に例を示す。これはtupleクラスを継承して、反復可能オブジェクトを受け取り、「(インデックス, 要素)」で構成されるタプルを要素とするタプルを生成するクラスだ。
class NumberedTuple(tuple):
def __new__(cls, iterable):
tmp = [(idx, value) for idx, value in enumerate(iterable)]
self = super().__new__(cls, tmp)
return self
nt = NumberedTuple(['foo','bar', 'baz'])
print(nt) # ((0, 'foo'), (1, 'bar'), (2, 'baz'))
__new__メソッドでは、enumerate関数を使って「(インデックス, 要素)」というタプルを要素とするリストを作成して、それを親クラス(tupleクラス)の__new__メソッドに渡している。これにより、番号付きのタプルを生成できるようになる。このような変更不可能な型を基に派生クラスを定義するような際には、__new__メソッドでインスタンス生成と初期化の処理を独自に行う必要があるだろう。
インスタンスを別の型のオブジェクトに変換するのに使える特殊メソッドもある。
特殊メソッド | 対応する操作/演算 | 説明 |
---|---|---|
__bool__(self) | bool関数 真偽テスト |
オブジェクトがTrue/Falseのどちらであるかを返す |
__bytes__(self) | bytes関数 | オブジェクトをバイト文字列で表現したものを生成する |
__complex__(self) | complex関数 | オブジェクトをcomplex型(複素数型)の値に変換する |
__float__(self) | float関数 | オブジェクトをfloat型の値に変換する |
__format__(self, format_spec) | format関数 f文字列化時 str.formatメソッド |
format_specに従ってオブジェクトを文字列化する |
__int__(self) | int関数 | オブジェクトをint型の値に変換する |
__repr__(self) | repr関数 | オブジェクトを表す公式な文字列を生成する |
__str__(self) | str関数 | オブジェクトを表す非公式な文字列を生成する |
インスタンスを別の型のオブジェクトに変換するための特殊メソッド |
__repr__メソッドと__str__メソッドはどちらも「オブジェクトを文字列化」する際に、repr関数やstr関数から呼び出されるが、その違いは前者は「オブジェクトの公式な文字列表現」を得るために、後者は「オブジェクトの非公式な文字列表現」を得るために使う点だ。「オブジェクトの公式な文字列表現」とは、eval関数などにその表現を渡すことで、元の値と同じものを得られるような表現(もしくは、詳細なオブジェクト情報)のこと。「オブジェクトの非公式な文字列表現」とはよりシンプルで読みやすい形でオブジェクトを表したものになる。object.__str__メソッドは、object.__repr__メソッドを呼び出すので、__repr__メソッドのみをオーバーライドして、__str__メソッドをオーバーライドしなかったときには、str関数やprint関数でそのオブジェクトを文字列化しようとすると、__repr__メソッドの結果が得られる。
以下に例を示す。このクラスは文字列として、数値を格納し、必要に応じてそれらを各データ型に変換できるようにしている(先頭にあるのは、float型の値に変換可能かを簡易的に調べるスタティックメソッド。float型に変換可能なら、int型にも変換できるので、ここでは受け取った文字列のチェックに使っている)。
class MyDigit:
@staticmethod
def _isfloat(val):
if val.startswith('-'):
val = val[1:]
if val.count('.') <= 1 and val.replace('.', '').isdigit():
return True
return False
def __init__(self, val="0.0"):
if MyDigit._isfloat(val):
self.val = val
else:
self.val = "0.0"
def __bool__(self):
return bool(float(self.val))
def __int__(self):
return int(float(self.val))
def __float__(self):
return float(self.val)
def __repr__(self): # eval関数でオブジェクトを復元可能な表現
return f"MyDigit('{self.val}')"
#def __str__(self): # コメントアウトを外すとself.valが返されるようになる
# return self.val
mydigit = MyDigit('foo')
print("MyDigit('foo'):", mydigit) # MyDigit('foo'): MyDigit('0.0')
print(f"bool(MyDigit('foo')):", bool(mydigit)) # bool(MyDigit('foo')): False
mydigit = MyDigit('1.5')
print(f'bool({mydigit}):', bool(mydigit)) # bool(MyDigit('1.5')): True
print(f'int({mydigit}):', int(mydigit)) # int(MyDigit('1.5')): 1
print(f'repr({mydigit}):', repr(mydigit)) # repr(MyDigit('1.5')): MyDigit('1.5')
print(f'str({mydigit}):', str(mydigit)) # str(MyDigit('1.5')): MyDigit('1.5')
print(f"eval('repr(mydigit)'):", eval('repr(mydigit)')) # eval('repr(mydigit)'): MyDigit('1.5')
ここではbool/float/int/repr関数にこのクラスのインスタンスを渡したときに、呼び出されるメソッドをオーバーライドした。実際のコードは簡単なので説明は省略する。基本的には、対応する演算を行って、その結果を文字列化したものからこのクラスのインスタンスを生成しているだけだ。注意したいのは、__str__メソッドはオーバーライドしていないので、str関数にこのクラスのインスタンスを渡すと、最終的にこのクラスの__repr__メソッドが呼び出されて、「MyDigit('1.5')」のような表現が得られるところだ。興味のある方は、__str__メソッドのコメントアウトを外して動作を確認してほしい。
オブジェクトの比較
2つのオブジェクトがあったときに、それらの値が等しいかどうか、同一のオブジェクトかどうかなどを比較できる。こうした目的でオーバーライドできる特殊メソッドとしては以下がある。
特殊メソッド | 対応する演算 | 説明 |
---|---|---|
__eq__(self, other) | ==演算子 | selfとotherの値が等しいかを調べる |
__ne__(self, other) | !=演算子 | selfとotherの値が等しくないかを調べる |
__hash__(self) | hash関数 | オブジェクトのハッシュ値を取得する |
__lt__(self, other) | <演算子 | selfがotherより小さいかどうかを調べる |
__le__(self, other) | <=演算子 | selfがother以下かどうかを調べる |
__gt__(self, other) | >演算子 | selfがotherよりも大きいかどうかを調べる |
__ge__(self, other) | >=演算子 | selfがother以上かどうかを調べる |
オブジェクトの比較を行うための特殊メソッド |
以下に例を示す。
class AAA:
def __init__(self, val=0):
self.val = val
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.val == other.val
else:
kls = other.__class__.__name__
raise NotImplementedError(
f'comparison between AAA and {kls} is not supported')
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if isinstance(other, self.__class__):
return self.val < other.val
else:
kls = other.__class__.__name__
raise NotImplementedError(
f'comparison between AAA and {kls} is not supported')
def __le__(self, other):
return self.__lt__(other) or self.__eq__(other)
def __gt__(self, other):
return not self.__le__(other) # other.__lt__(self)
def __ge__(self, other):
return not self.__lt__(other) # other.__le__(self)
aaa = AAA(10)
bbb = AAA(20)
print(aaa == bbb) # False
print(aaa != bbb) # True
print(aaa < bbb) # True
print(aaa <= bbb) # True
print(aaa > bbb) # False
print(aaa >= bbb) # False
aaa > 100 # NotImplmentedError例外
この例では、__lt__メソッドと__eq__メソッドで詳細な定義を行い、他のメソッドは2つのメソッドを基にそれぞれの比較結果を返すようにしている。__lt__メソッドと__eq__メソッドでは、比較対象(other)が自分のクラス(AAA)もしくはその派生クラスであれば比較できるようにisinstance関数を使って型チェックを行い、クラス階層に含まれないインスタンスであれば、NotImplementedError例外を発生するようにしてある。
__le__メソッドが定義されていて__ge__メソッドが定義されていないとき(またはその逆)、あるいは__lt__メソッドが定義されていて__gt__メソッドが定義されていないとき(またはその逆)には、selfとotherを入れ替えてもう一方のメソッドが呼び出される。これは例えば「a > b」という比較を行いたかったが__gt__メソッドがないので、「b < a」という比較を行い、結果、__lt__メソッドが呼び出されるということだ。
なお、functoolsモジュールのtotal_orderingデコレータを使用することで、上記のクラス定義は以下のようにも書ける(total_orderingデコレータでクラス定義を修飾した上で、__eq__メソッドと、__lt__/__le__/__gt__/__ge__の4つのメソッドのいずれか1つ、合わせて2つのメソッドを定義すればよい)。
from functools import total_ordering
@total_ordering
class AAA:
def __init__(self, val=0):
self.val = val
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.val == other.val
else:
kls = other.__class__.__name__
raise NotImplementedError(
f'comparison between AAA and {kls} is not supported')
def __lt__(self, other):
if isinstance(other, self.__class__):
return self.val < other.val
else:
kls = other.__class__.__name__
raise NotImplementedError(
f'comparison between AAA and {kls} is not supported')
呼び出し可能オブジェクト
クラスを定義したり、関数を定義したりすると、それらはかっこ「()」を付加し、そこ中に引数を指定することで、「呼び出す」ことができるようになる。こうしたオブジェクトのことを「呼び出し可能オブジェクト」と呼ぶ。
Pythonでは通常、クラスは呼び出し可能オブジェクトだが、そのインスタンスは呼び出し可能ではない。しかし、インスタンスを呼び出し可能オブジェクトのようにもできる。これには__call__特殊メソッドを定義すればよい。
特殊メソッド | 対応する操作 | 説明 |
---|---|---|
__call__(self, args) | 「self(args)」形式の関数呼び出し | オブジェクトを関数として呼び出す |
インスタンスを呼び出し可能にする特殊メソッド |
以下に例を示す。
class Hello:
def __call__(self, x):
print(f'Hello {x}')
hello = Hello() # クラスは呼び出し可能オブジェクト
hello('world') # Helloクラスのインスタンスは呼び出し可能オブジェクト
数値演算
加減乗除と整数除算などをカスタマイズするための特殊メソッドもある。
特殊メソッド | 対応する操作 | 説明 |
---|---|---|
__neg__(self) | -演算子(単項) | 符号反転に対応する操作を定義 |
__sub__(self, other) | -演算子(二項) | 減算に対応する操作を定義 |
__pos__(self) | +演算子(単項) | 数値の符号を変更しない操作に対応する操作を定義 |
__add__(self, other) | *演算子(二項) | 加算に対応する操作を定義 |
__mul__(self, other) | *演算子 | 乗算に対応する操作を定義 |
__truediv__(self, other) | /演算子 | 除算に対応する操作を定義 |
__floordiv__(self, other) | //演算子 | 整数除算(商)に対応する操作を定義 |
__mod__(self, other) | %演算子 | 整数除算(剰余)に対応する操作を定義 |
__divmod__(self, other) | divmod関数 | divmod関数から呼び出される |
__pow__(self, other[, modulo]) | pow関数/**演算子 | 累乗に対応する操作を定義 |
__lshift__(self, other) | <<演算子 | 左シフト演算に対応する操作を定義 |
__rshift__(self, other) | >>演算子 | 右シフト演算に対応する操作を定義 |
__and__(self, other) | &演算子 | ビット単位のAND演算に対応する操作を定義 |
__xor__(self, other) | ^演算子 | ビット単位のXOR演算に対応する操作を定義 |
__or__(self, other) | |演算子 | ビット単位のOR演算に対応する操作を定義 |
__abs__(self) | abs関数 | 絶対値の取得に対応する操作を定義 |
数値演算をカスタマイズする特殊メソッド(一部) |
以下に例を示す。
class MyDigit:
@staticmethod
def _isint(val):
if val.startswith('-'):
val = val[1:]
if val.count('.') == 0 and val.isdigit():
return True
return False
def __init__(self, val="0.0"):
if MyDigit._isint(val):
self.val = val
else:
self.val = "0"
def __add__(self, other):
if isinstance(other, self.__class__):
tmp = str(int(self.val) + int(other.val))
return MyDigit(tmp)
else:
raise NotImplementedError()
def __sub__(self, other):
if isinstance(other, self.__class__):
tmp = str(int(self.val) - int(other.val))
return MyDigit(tmp)
else:
raise NotImplementedError()
def __mul__(self, other):
if isinstance(other, self.__class__):
tmp = str(int(self.val) * int(other.val))
return MyDigit(tmp)
else:
raise NotImplementedError()
def __floordiv__(self, other):
if isinstance(other, self.__class__):
tmp = int(self.val) // int(other.val)
return MyDigit(str(tmp))
else:
raise NotImplementedError()
def __mod__(self, other):
if isinstance(other, self.__class__):
tmp = int(self.val) % int(other.val)
return MyDigit(str(tmp))
else:
raise NotImplementedError()
def __truediv__(self, other):
return self.__floordiv__(other)
def __divmod__(self, other):
q = self.__floordiv__(other)
r = self.__mod__(other)
return (q, r)
def __neg__(self):
if self.val.startswith('-'):
tmp = self.val[1:]
else:
tmp = "-" + self.val
return MyDigit(tmp)
def __str__(self):
return self.val
d1 = MyDigit('17')
d2 = MyDigit('4')
print(d1 + d2) # 21
print(d1 - d2) # 13
print(d2 - d1) # -13
print(d1 * d2) # 68
print(d1 / d2) # 4
print(d1 % d2) # 1
(q, r) = (divmod(d1, d2))
print(q, r) # 4 1
print(-d1) # -17
この例では、数値を文字列の形で保存し、その四則演算を行うように特殊メソッドをオーバーライドした(加えて、値を簡単に表示できるように__str__メソッドもオーバーライドしている)。多くのメソッドでは、otherがこのクラス(かその派生クラス)のインスタンスであることを確認してから演算を行ってその結果を返送し、そうでない場合には先ほどと同様にNotImplementedError例外を発生させるようにしてある。それ以外は、対応する演算を行って、そこからこのクラスのインスタンスを新たに生成し、それを戻り値として渡すようにしているだけだ。
Copyright© Digital Advantage Corp. All Rights Reserved.