本記事はPython Advent Calendar 202014日目の記事です。
Pythonの本を読んでいたらNotImplementedという記述が出てきました。
初めて見るビルトインのものだったのでこのNotImplementedについて色々調べてみます。
公式ドキュメントによると・・・
Python公式ドキュメントの「組み込み定数」のページに以下の記述がありました。
二項演算の (あるいはインプレースの) メソッドが NotImplemented を返した場合、インタープリタはもう一方の型で定義された対の演算で代用を試みます (あるいは演算によっては他の代替手段も試みます)。試行された演算全てが NotImplemented を返した場合、インタープリタは適切な例外を送出します。
組み込み定数
どうやらTrueやFalse、None、Ellipsisなどと同じようなビルトインの組み込み定数なようです。TrueやNoneなどは有名ですが、今まで存在を知りませんでした。
ドキュメントには「メソッドが NotImplemented を返した場合、インタープリタはもう一方の型で定義された対の演算で代用を試みます」と書かれています。これはどういった挙動をするものでしょう?
コードを書いて試してみる
等価の演算子(==
)を使う形でコードを試してみます。クラスで等価演算子の内容を定義するには__eq__
のメソッドを記述することで対応ができます。
AとBというクラスを作成し、A側の__eq__
メソッドでは、(実際にはこういった内容が必要になることはほぼ無い変なコードですが検証ようとして)もし比較として指定された引数の値(obj
)がAクラスのインスタンスもしくはBクラスのインスタンスであればTrueを返すようにしています。
一方でB側の__eq__
メソッドではダイレクトにNotImplementedを返すようにしてあります。
※実行されたことが分かるように各
class A:
def __eq__(self, obj: object) -> bool:
print('A側の__eq__メソッドが実行されました。')
if isinstance(obj, A):
return True
if isinstance(obj, B):
return True
return False
class B:
def __eq__(self, obj: object) -> bool:
print('B側の__eq__メソッドが実行されました。')
return NotImplemented
a = A()
b = B()
Aクラスのインスタンスをa、Bクラスのインスタンスをbとしました。
まずはaとa同士、及びaとbを比較してみます。これは当たり前ですが普通にTrueになります。また、左辺側のメソッドが先に評価されるため、a側のメソッドのみが実行されていることが出力内容から分かります。
print('a == a:', a == a)
print('a == b:', a == b)
A側の__eq__メソッドが実行されました。
a == a: True
A側の__eq__メソッドが実行されました。
a == b: True
ではB側を前にする形でb == a
とするとどうでしょう?
b側を左辺側に置いているので、NotImplementedが返却されるb側の__eq__
メソッドが実行されます。
print('b == a:', b == a)
B側の__eq__メソッドが実行されました。
A側の__eq__メソッドが実行されました。
実行してみると、b側だけでなくa側のメソッドも実行されていることが分かります。このようにNotImplementedを使うことで「先に評価した側でNotImplementedが返却された場合には右辺側のメソッドも使って評価される」という挙動を実装することができます。
どんなケースで便利なのか
今まで知らなかったというのもあり、正直ビルトインや各種ライブラリの内部実装で不足していない感じではありますが、もし自分でなにか作る際には既存のもの(ビルトインのもの)などとの比較で、別のクラスのインスタンス同士を比較するケースなどで便利かなと思いました。
たとえば自作のクラスなどで、属性(_list_val
)にリストを持つようなクラスが必要になったとします。以下のような感じです。
class OriginalArray:
def __init__(self, list_val: list) -> None:
self._list_val = list_val
このとき、このインスタンス自体の比較で内部の属性のリストを使いたいとします。
そのままリストと比較するとリストの値が一致していてもFalseになってしまいます。
list_val = [100, 200, 300]
original_arr = OriginalArray([100, 200, 300])
print(list_val == original_arr)
False
そこで、追加したクラスに__eq__
メソッドを追加してリストと比較ができるようにするとします。
class OriginalArray:
def __init__(self, list_val: list) -> None:
self._list_val = list_val
def __eq__(self, obj: object) -> bool:
print('OriginalArrayの__eq__メソッドが実行されました。')
if isinstance(obj, list):
return obj == self._list_val
if isinstance(obj, OriginalArray):
return obj._list_val == self._list_val
return False
ビルトインのリスト側ではOriginalArrayクラスとの比較はNotImplementedになるので、以下のように比較してもOriginalArray側の__eq__
メソッドが実行され、等価条件の比較を行ってくれます。
list_val = [100, 200, 300]
original_arr = OriginalArray([100, 200, 300])
print(list_val == original_arr)
OriginalArrayの__eq__メソッドが実行されました。
True
これがもしビルトイン側でNotImplementedが指定されていない場合(ビルトイン側のクラスの__eq__
メソッドだけでしか判定去れない場合)、list_val == original_arr
とoriginal_arr == list_val
で、比較順の違いだけで結果が変わってしまいます。
サードパーティのライブラリなどを作る際にもそれだと不便なので、拡張しやすいように良く考えられているなぁという印象です。
使った環境
- Python 3.8.5