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;

コードリーディング観察日記

以前コードリーディングの方法を聞かれて「僕、全然読めないよ」と答えたんだけど、そういう解釈はさておき客観的事実としては30分の空き時間で「Pythonクラスのメソッド名解決順序」に書いてあるようなことを調べられたわけなので、そのプロセスがどうであったか忘れないうちに観察日記をつけておく。

観察日記

  • (調べたい内容が、Pythonのリファレンスマニュアルに書いてあった気がしない)
  • Pythonの処理系で対話的に実行して、挙動を確認。「__bases__を書き換えた時に__mro__を更新するはずだ」と考える
  • ソースコードは既にダウンロード済み(~/src/にソースを読もうと思ったもののソースコードは入れっぱなしになっている)
  • Objects/classobject.cを開く。__bases__で検索する。set_basesって関数を見つける。読む。期待に反して__mro__の更新が行われていない。おかしい。
  • ./configure --with-pydebug してデバッグ情報付きのバイナリを作る。set_basesにブレークポイントをつけて実行。set_basesが呼ばれると考えたPythonコードを実行してもブレークされない。期待に反して呼ばれてない。
  • ソースルートで ack set_bases する。typeobject.cにtype_set_basesなんてのがあるじゃん!そうか、新スタイルのクラスはtypeobject.cか。
  • ack type_set_bases する。type_set_basesを読む。mroって単語が出てくるところだけ流し読みする。
  • r = mro_subclasses(type, temp);が見つかる。
  • mro_subclassesが自分自身とサブクラスのmroを書き換えるかなと期待したが、期待に反してmro_subclassesの中ではサブクラスをたどりながらmro_internalを呼び出すだけ。
  • mro_internalがmroを書き換える処理なのか、と思ってtype_set_basesを読みなおしてみると、mro_internalを読み落としていたのが見つかった。
  • 今思えばmroで検索してハイライトしとけよ自分
  • mro_internalを読んでみたらmro_implementationってのを読んでる。mro_implementationを読んだらさがしていたものはこれっぽい。調査終了。
  • PyTypeObjectの定義を書いておこうと考える。Include/type...h を開こうとしたがそんな名前のファイルはない。ack PyTypeObjectする。いっぱい出すぎる。そりゃそうだ。ack PyTypeObject Include/*.hする。object.hのなかにあった。

使用したツール

  • gdb
    • b(reakepoint)とr(un)しか使ってない
  • ack
  • emacs
    • タグジャンプ使いこなせてない…。
    • インクリメンタルサーチ(Ctrl-sやCtrl-r)と、行番号ジャンプ(僕はCtrl-lにバインドしている)で。
    • あっ、EmacsでのM-x pdbもこの前教えてもらったのに使うの忘れてた。生でgdbつかってたや。

まとめ

やっぱりほとんど読んでない。100行未満。

もっと手練の人の観察日記を読みたい!