🔧

Pythonのクラスでカスタマイズできる特殊メソッド

2021/04/18に公開

Zenn初投稿です。これまでQiitaやはてなブログで記事を書いてました。よろしくお願いします。

概要

Pythonのクラスを作るときにカスタム実装可能な特殊メソッドについて書いてみます。

特殊メソッドとは

一言でいうと__init__のようなメソッドのことです。クラスを定義するときにdefで特殊メソッドを定義してやると、クラスが[]+などのオペレータやint()などの型変換に渡されたときの処理を実装することができます。

特殊メソッド一覧

コンストラクタ(__init__)

一番基本的なメソッドです。これは書いたこともある人も多いかと思います。myclass()でインスタンスが生成されたときの初期化処理を記述します。

厳密には__new__というメソッドでインスタンスが生成されて__init__で初期化が行われます。なので仕様として__init__ではNone以外の値をreturnすることは許されません(Python公式ドキュメントより)。

関数呼び出し(__call__)

__call__は()をオーバーライドします。つまりmyobj()で行われる処理を上書きすることができます。

個人的には__call__をオーバーライドするのは好きじゃないです。なぜならmyobj()のような処理では一般に何を行うかが読み取りにくく、可読性が落ちるからです。

よくわかる具体例としてTensorFlowとPyTorchでの推論の書き方の違いがあります。ディープラーニングモデルに推論させるとき、TensorFlowでは

yy = mymodel.predict(xx)

のように書きます。これは「mymodelがpredictする」と読めるので、TensorFlowをあまり知らない人でも何をやる処理なのか一見してわかります。

それに対してPyTorchでは推論は__call__する形で記述します。

yy = mymodel(xx)

PyTorchを知らない人が見ると、これを一見して「推論を行う処理だ」と理解するのは難しいです。PyTorchのようなメジャーなフレームワークは調べればわかるのですが、独自クラスでこういう記述をすると訳が分からないことになりうるので、よほどわかりやすく記述できるときでないと触らないのが無難だと思います。

単項演算子、組み込み型への変換(__int__など)

単項演算子

メソッド名 演算子 説明
__pos__ + 正数
__neg__ - マイナス
__abs__ abs() 絶対値
__invert__ ~ ビット反転

組み込み型への変換

メソッド名 演算子 説明
__int__ int() 整数
__float__ float() 実数
__complex__ complex() 複素数
__str__ str() 文字列

「listがないな」と思うかもしれませんが、list型への変換は後述する__iter__を実装してイテレートできるようにしてやると可能になります。listのコンストラクタがiteratorオブジェクトを引数とするためです。

二項演算子のオーバーロード(__add__など)

+や*のような二項演算を定義します。

算術演算

メソッド名 演算子 説明
__add__ + 加法(たし算)
__sub__ - 減法(ひき算)
__mul__ * 乗法(かけ算)
__matmul__ @ 行列積
__truediv__ / 除法(わり算)
__floordiv__ // 整数除法
__mod__ % 剰余
__divmod__ divmod() 整数除法と剰余の同時計算
__pow__ pow() べき乗

論理演算

メソッド名 演算子 説明
__and__ & 論理積(ビット演算のand)
__or__ | 論理和(ビット演算のor)
__xor__ ^ 排他的論理和(ビット演算のxor)
__lshift__ << 左シフト
__rshift__ >> 右シフト

比較演算

メソッド名 演算子 説明
__eq__ == 等価比較
__ne__ != 不等比較
__lt__ < 小なり
__gt__ > 大なり
__le__ <= 小なりイコール
__gt__ >= 大なりイコール

これらを実装する場合はbool型をreturnするようにしておかなければいけません。

また、結果が矛盾しないようにすることに注意する必要があります。デフォルトでは__ne__(!=)は__eq__(==)の逆の値を返すようになっていますが、__ne__を自前で実装する場合は必ずしもそうなるとは限りません。さらに(x<y or x==y)x<=yの結果が一致しないこともありえます。

複合代入演算子のオーバーロード(__iadd__など)

+=のような複合代入処理を定義します。

メソッド名 演算子 説明
__iadd__ += 加法(たし算)
__isub__ -= 減法(ひき算)
__imul__ *= 乗法(かけ算)
__imatmul__ @= 行列積
__itruediv__ /= 除法(わり算)
__ifloordiv__ //= 整数除法
__imod__ %= 剰余
__ipow__ **= べき乗
__iand__ &= 論理積(ビット演算のand)
__ior__ =
__ixor__ ^= 排他的論理和(ビット演算のxor)
__ilshift__ <<= 左シフト
__irshift__ >>= 右シフト

二項演算子の入れ替え

例えばx+yx.__add__(y)を呼び出しますが、x__add__が実装されていないとエラーになります。このときy__radd__が定義されている場合、y.__radd__(x)が呼び出されます。このような逆向きの演算も実装できます。

(表は疲れたので割愛します。。。)

コンテナ的な動きをさせるためのメソッド(__len__など)

listやdictなどのコンテナのような挙動をさせるためのメソッドです。

メソッド名 演算子 説明
__len__ len() 長さ
__iter__ iter() イテレートするときの挙動
__getitem__ [] 要素の取り出し(代入は不可)
__setitem__ [] 要素の代入

__getitem__だけ実装しても代入はできません。代入するときは__setitem__も定義する必要があります。

その他組み込み関数への対応

__repr__

printされたときに返す文字列を定義します。

__bytes__

bytesで呼び出されたときのバイト表現を定義します。

__format__

format関数で呼び出されたときの文字列を定義します。

with文への対応

__enter__でwithブロックに入った時の挙動、__exit__でwithブロックが終了したときの挙動を実装することができます。

使いこなすには

どのメソッドがどういう式を書いたときに呼び出されるか」を理解するのがポイントです。

特殊メソッドのオーバーロードは記号を使った書き方を増やす手法なので、注意しないと__call__のように、わかりにくい記法を増やして読みにくいコードが書かれてしまうことにつながります。なので「その特殊メソッドが使われたときどのようなコードが書かれうるか」を想像してから使うべきだと思います。

# こんなコードが許容されるべきか?(これが通る実装は可能ではあるが)
ret = myobj1()[mykey] + int(myobj2()()[0:2]) 

参考サイト

Discussion