Pythonクラスのメソッド名解決順序

昨日のプロシンで「枝分かれのあるプロトタイプチェーンがあるときにメソッド名の解決はどうなるのか」「PythonではC3直列化でシリアライズして端から探索していく」という話をしたのですけど、PythonのC3直列化が入ったクラスが親クラスを書き換え可能かどうか自信がなかったのでその場では断言出来なかったので、確認して見ました。

Pythonのクラスは親クラスを単なるポインタとして持っていて、定義後に親クラスを変更できるので、プロトタイプチェーンと言って構わないかと思います。で、それでダイヤモンド継承を作ってみます。クラスAをBとCが継承した上で、DがBとCをこの順で継承します。__mro__という属性にMethod Resolution Orderが保存されているのがわかります。

>>> class A(object): pass
>>> class B(A): pass
>>> class C(A): pass
>>> class D(B, C): pass
>>> D.__mro__
(__main__.D, __main__.B, __main__.C, __main__.A, object)

クラスDの親クラスが何かという情報は__bases__属性の中に保管されています。なのでこれを書き換えます。DがCとBをこの順で継承するようにすると、MROの中でBよりもCが先に来るように変わりました。

>>> D.__bases__
>>> (__main__.B, __main__.C)
>>> D.__bases__ = (C, B)
>>> D.__mro__
(__main__.D, __main__.C, __main__.B, __main__.A, object)

このC3直列化が単なる幅優先ではないのを確認するためにクラスEを追加してそれも継承するようにしてみます。幅優先探索ならC, B, E, Aの順で検索するわけですが、PythonではC, B, A, Eの順になっていることがわかります。

>>> class E(object): pass
>>> D.__bases__ = (C, B, E)
>>> D.__mro__
(__main__.D, __main__.C, __main__.B, __main__.A, __main__.E, object)

__bases__に値を設定したタイミングで__mro__が計算され直されます。typeobject.cのtype_set_basesの中からmro_internalやmro_subclassesが呼び出されて、mro_internalからmro_implementationが呼び出されて、ここで計算しなおしている。つまり、あるクラスの親クラスを変更すると、そのクラスとすべてのサブクラスのMROが更新されるわけです。

参考: PyTypeObjectの定義

typedef struct _typeobject {
    (略)
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    (略)
} PyTypeObject;