たごもりすメモ

コードとかその他の話とか。

クラスメソッド入れ替えに伴うバグ?

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)
>>> 

駄目だあ。どうしよ。