Edited at

Pythonの演算子のOverloadの優先順位について

More than 3 years have passed since last update.


概要


  • 先日 PyCon JP 2016 で発表する機会をいただいたので発表してきました。

  • 発表資料 「メタプログラミングPython

  • その時、演算子の Overload の優先順位について質問されたけど、パッと回答できなかったおじさんなので簡単にまとめました。


質問



  • __add__ を定義したオブジェクトA と __radd__ を定義したオブジェクトBはどっちのメソッドが優先されますか?

class A:

def __init__(self, value):
self.value = value

def __add__(self, other):
self.value += other.value
return self

class B:

def __init__(self, value):
self.value = value

def __radd__(self, other):
self.value += other.value
return self

A(1) + B(2) #=> Aの __add__ が動くのか? B の __radd__が動くのか?


回答


  • 左側の A.__add__ が優先されて実行されます。

  • 基本的に __radd__ というのは __add__ が未実装の場合のfallback用のメソッドだというのが実態のようです。

  • この辺は私もちゃんと理解できていなくて、誤った説明をしてしまいました。すいません。

A(1) + B(2) #=> A(3)


#=> Aクラスには __add__ が実装されているのでまずそちらが動く


どういうことなのか?

ドキュメントには __rxxx__ 系 メソッドの説明には以下のような説明が書いてあります。

これらのメソッドを呼んで二項算術演算の、被演算子が反射した (入れ替えられた) ものを実装します。

これらの関数は、左側の被演算子が対応する演算をサポートしておらず、 非演算子が異なる型の場合にのみ呼び出されます。
例えば、y が __rsub__() メソッドのあるクラスのインスタンスである場合、
式 x - y を評価すると x.__sub__(y) が NotImplemented を返すときは y.__rsub__(x) が呼ばれます。

# refs http://docs.python.jp/3/reference/datamodel.html#object.__ror__

また注釈として以下のようにも書かれています。

注釈 右側の被演算子の型が左側の被演算子の型のサブクラスであり、

このサブクラスであるメソッドに対する反射メソッドが定義されている場合には
、左側の被演算子の非反射メソッドが呼ばれる前に、このメソッドが呼ばれます。
この振る舞いにより、サブクラスが親の演算をオーバーライドすることが可能になります。

これを順に確認してみます。


メモ

- NotImplemented とは「対応していない型に対して比較演算や二項演算が行われた時に返す用の組み込み型」

- __add__ に対する __radd__ のようなメソッドのことを反射メソッドと呼ぶ


1. __add__ が未実装であれば __radd__ が実行

A + B

このコードは下記の順番で実行されます。

1. 「A.__add__」 が実行され「B」が加算される

2. 1で「NotImplemented」が返されたら 「B.__radd__」 を実行して 「A」 を加算する

1と2のタイミングでそれぞれ NotImplemented を返さないのであれば、加算された結果を返して処理は終了します。 演算結果で最終的に NotImplemented が返ってくると TypeError が返されます

TypeError: unsupported operand type(s) for +: 'A' and 'B'

これをコードで確認してみます。

class A:

def __add__(self, other):
print('1. A.__add__ return NotImplemented')
return NotImplemented

class B:

def __radd__(self, other):
print('2. B.__radd__ return NotImplemented')
return NotImplemented

A() + B()

このコードを実行すると以下のようになります。


実行結果

1. A.__add__ return NotImplemented

2. B.__radd__ return NotImplemented
Traceback (most recent call last):
File "oeprator_loading.py", line 32, in <module>
A() + B()
TypeError: unsupported operand type(s) for +: 'A' and 'B'

どうやら上で書いた順番の通り動いてるようです。

ですが例外 TypeError は一体どこで投げられてるのでしょう? A.__add__B.__radd__NotImplemented を返してるだけなのに?

そこでCPythonのコードの中身を探してみます。すると下記のようなコードが見つかりました。

static PyObject *

binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name)
{
PyObject *result = binary_op1(v, w, op_slot);
if (result == Py_NotImplemented) {
Py_DECREF(result);
return binop_type_error(v, w, op_name);
}
return result;
}
// refs https://github.com/python/cpython/blob/3.6/Objects/abstract.c#L806

binop_type_error が実際に TypeError を吐いてるようで、下記のような流れになっています。

1. + 演算子の処理が実行される

2. 「binary_op1」 の中で 「__add__ or __radd__」 を実行して最終的に「NotImplemented」受け取る
3. 「NotImplemented」 を受け取ったら 「binop_type_error」 で 「TypeError」 を吐く

これで一連の流れが確認できました。次に注釈部分の確認をします


2.サブクラスであれば __radd__ が先に実行される

注釈 右側の被演算子の型が左側の被演算子の型のサブクラスであり、

このサブクラスであるメソッドに対する反射メソッドが定義されている場合には
、左側の被演算子の非反射メソッドが呼ばれる前に、このメソッドが呼ばれます。
この振る舞いにより、サブクラスが親の演算をオーバーライドすることが可能になります。

本当にこんな動きになるのか確認します。下記のようなコードを考えてみます。

class A:

def __add__(self, other):
print('1. A.__add__ return 10')
return 10

class B:

def __radd__(self, other):
print('2. B.__radd__ return NotImplemented')
return NotImplemented

A() + B()


結果

1. A.__add__ return 10


この場合 A.__add__return 10 を返すだけなので B.__radd__ は実行されません。ここで BクラスをAクラスから継承してみます。

class A:

def __add__(self, other):
print('1. A.__add__ return 10')
return 10

class B(A): # <- Aクラスを継承

def __radd__(self, other):
print('2. B.__radd__ return NotImplemented')
return NotImplemented

A() + B()


結果

2. B.__radd__ return NotImplemented

1. A.__add__ return 10

すると B.__radd__ が先に実行されていて「演算子を挟んで右側が左側のサブクラスであれば、優先的に反射メソッドが実行される」というのが確認できました。

また B.__radd__ が優先的に実行されるというだけで、 NotImplemented が返されれば、 A.__add__ に fallback されるということもわかりました。


3. __iadd__ is 何?

これらのメソッドを呼び出して累算算術代入 (+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=) を実装します。

これらのメソッドは演算をインプレースで (self を変更する) 行うよう試み、
その結果 (その必要はありませんが self でも構いません) を返さなければなりません。
特定のメソッドが定義されていない場合、その累算算術演算は通常のメソッドにフォールバックされます。
例えば x が __iadd__() メソッドを持つクラスのインスタンスである場合、x += y は x = x.__iadd__(y) と等価です。
そうでない場合、x + y の評価と同様に x.__add__(y) と y.__radd__(x) が考慮されます。
特定の状況では、累算代入は予期しないエラーに終わるかもしれません
(なぜ加算はされるのに a_tuple[i] += [‘item’] は例外を送出するのですか? を参照してください) が、
この挙動は実際はデータモデルの挙動の一部です。

# refs http://docs.python.jp/3/reference/datamodel.html#object.__ior__

累算算術代入」の処理をOverloadできます。つまり「A += B」の演算をカスタマイズできるということです。


まとめ


  • 演算子の Overload は 「1. 演算子メソッド -> 2. 反射メソッド」の順番で実行される


  • 反射メソッド は 演算子メソッドが NotImplemented を返した場合に初めて実行される

  • 演算結果が NotImplemented になると TypeError: unsupported operand type(s) for .. が送出される。


  • 累算算術代入 だけをOverloadできる スペシャルメソッド(__ixxx__) が存在している

いくつか他に質問をもらいましたが、それはまた後日どっかに書こうかと思います。

おわりでやんす


参考