AppEngine用のテスト基盤を作るのにあれこれやってたら、Pythonのバグ?らしきものを見付けたのでメモ。Python2.5でも2.6でも起きる。
あとで本当にバグなのか仕様なのかを調べる。仕様だったらやだなあ……と思ったが、バグでも、これ一朝一夕には直らない気がするぞ。
クラスメソッド入れ替え
要するに以下のような動作っておかしいよね? という話。以下コード。
~$ python2.6 Python 2.6.4 (r264:75706, Dec 7 2009, 18:45:15) [GCC 4.4.1] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> class C(object): ... @classmethod ... def name(cls): ... return "a" + cls.__name__ ... >>> orig_name = C.name >>> orig_name <bound method type.name of <class '__main__.C'>> >>> class D(C): pass ... >>> D.name <bound method type.name of <class '__main__.D'>> >>> D.name() 'aD' >>> def xname(cls): ... return "x" + cls.__name__ ... >>> C.name = classmethod(xname) >>> C.name <bound method type.xname of <class '__main__.C'>> >>> D.name <bound method type.xname of <class '__main__.D'>> >>> C.name() 'xC' >>> D.name() 'xD' >>> D.name <bound method type.xname of <class '__main__.D'>> >>> C.name = orig_name >>> C.name <bound method type.name of <class '__main__.C'>> >>> D.name <bound method type.name of <class '__main__.C'>> >>> D.name() 'aC' >>>
最後に C.name に orig_name を戻した時点で D.name の中身が "class Cにバインドされた name メソッド"に化けてる。なので D.name() で呼び出したとき、レシーバ cls が C になってしまい、想定外の結果になってしまう。
ちなみに、このあと更に classmethod 指定つきでメソッドを C.name に代入すると、期待した通りの結果が再び得られる。
>>> def yname(cls): ... return "y" + cls.__name__ ... >>> C.name = classmethod(yname) >>> C.name <bound method type.yname of <class '__main__.C'>> >>> D.name <bound method type.yname of <class '__main__.D'>> >>>
うーん、つまり取り出したものを戻すときだけおかしくなるってことか? ちゃんとclassmethodでくるんで代入しろ、と。
でも取り出したものはもうclassmethodでくるまれた後のものだしなあ。試しにもう一回くるんで入れてみるか。
>>> C.name = classmethod(orig_name) >>> C.name <bound method type.name of <class '__main__.C'>> >>> D.name <bound method type.name of <class '__main__.D'>> >>> C.name() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: name() takes exactly 1 argument (2 given) >>>
駄目だあ。どうしよ。