メソッドオブジェクトの不思議とid()の落とし穴
さて、@aroma_blackさんがこんなスクリプトで悩んでおられたのである。
class C(object): def foo(self): pass c = C() print id(c.foo) == id(c.__class__.foo) print c.foo is c.__class__.foo
@aroma_blackさんはメソッドオブジェクトがどこに隠れているのか調べていたようだ。この二つのprint
文で出力される結果はおわかりだろうか。
c.foo is c.__class__.foo
まず、二番目のprint
文から見てみよう。
c.foo
は見た通り、C
クラスのインスタンス c
からメソッド foo
を取得する式である。
>>> c.foo <bound method C.foo of <__main__.C object at 0x02A8B3B0>>
c.foo
では、bound method
と呼ばれる、instancemethod
型のオブジェクトが返る。bound method
は通常の関数オブジェクトとよく似ているが、メソッドを取得したインスタンス(ここでは c
オブジェクト) を記録している。この記録したインスタンスは、メソッドを呼び出したときに第一引数のself
として渡されるオブジェクトになる。
このbound method
オブジェクトは、メソッドごとに一つしか存在しないオブジェクトではない。単純に考えれば、最初にfoo
メソッドのオブジェクトを作っておき、呼び出されるたびにそのオブジェクトを返せば良いだろう。しかし、実際には
>>> [hex(id(m)) for m in [c.foo for i in range(3)]] ['0x282d3c8', '0x2a85968', '0x282d350']
と c.foo
を3回取り出すと、3回とも別々のオブジェクトが返ってくる。つまり、Pythonはメソッドを呼び出すたびに、新しいbound method
オブジェクトを生成しているのだ。
なぜ作り置きせずにいちいち生成しているのだろうか?bound method
オブジェクトは生成元のインスタンスを記録している、と説明したが、これに加えてインスタンスが生成したbound method
オブジェクトを記録すると何が起こるだろう?そう、循環参照だ。bound method
オブジェクトとインスタンスがお互いに参照を持ち合うため、循環参照が発生してPythonの参照カウントでメソッドとインスタンスを解放することができなくなってしまうのだ。
これではせっかくの参照カウントによるガベージコレクションが全くの無駄になってしまうので、Pythonではbound method
オブジェクトを参照されるたびに生成し、インスタンスにはbound method
オブジェクトへの参照を持たないようにしているのである。
c.__class__.foo
はC.foo
と等価で、クラスオブジェクトからメソッドを取得する式である。このようにクラスから取り出されたメソッドはunbound method
オブジェクトと呼ばれ、bound method
と同様に毎回生成される。ちなみにこのunbound method
メソッドはPython3で捨てられた要らない子なので、特に解説はしない。
というわけで、二番目のprint
文
print c.foo is c.__class__.foo
はFalse
となる。c.foo
とc.__class__.foo
は、どちらも毎回新しいinstancemethod
型のオブジェクトを生成するので、is
演算子で一致することはあり得ないのである。
id(c.foo) == id(c.__class__.foo)
次に1番目のprint
文を見てみよう。二つのinstancemethod
オブジェクト c.foo
と c.__class__.foo
の id
を取得し、比較する式だ。それぞれ別々のオブジェクトの id
を比較しているのだから当然False
となる………だろうか?
id()
の説明をちょっと読んでみよう。
id(object)
http://www.python.jp/doc/2.5/lib/built-in-funcs.html
オブジェクトの ``識別値'' を返します。この値は整数 (または長整数) で、このオブジェクトの有効期間は一意かつ定数であることが保証されています。 オブジェクトの有効期間が重ならない 2 つのオブジェクトは同じ id() 値を持つかもしれません。 (実装に関する注釈: この値はオブジェクトのアドレスです。)
オブジェクトのid
は一意だが、それはオブジェクトの有効期間に限るのだ。id(c.foo)
と書くと、
という順序で動作する。
次のid(c.__class__.foo)
でも同様に
となるが、ここでc.__class__.foo
オブジェクトのid
を取得するときには、既にc.foo
メソッドは解放されてしまっているため、id
が一意になるとは保証されないのである。bound method
もunbound method
も実態は同じinstancemethod
型オブジェクトなので、現在のCPythonの実装では、c.__class__.foo
を生成するときに、その直前に解放されたc.foo
オブジェクトが使い回される可能性が高い。
このため、id(c.foo) == id(c.__class__.foo)
と書いてしまうと、実際には別々のオブジェクトであるにも関わらず、この式はTrue
となってしまうことになる。id
を比較する場合には、どちらのオブジェクトも解放されてしまわないように
f1 = c.foo f2 = c.__class__.foo print id(f1) == id(f2)
のように書かなければならない。