概要
- 先日 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__
) が存在している
いくつか他に質問をもらいましたが、それはまた後日どっかに書こうかと思います。
おわりでやんす