読者です 読者をやめる 読者になる 読者になる

メソッドオブジェクトの不思議とid()の落とし穴

さて、@さんがこんなスクリプトで悩んでおられたのである。

class C(object):
    def foo(self):
        pass

c = C()

print id(c.foo) == id(c.__class__.foo)
print c.foo is c.__class__.foo

@さんはメソッドオブジェクトがどこに隠れているのか調べていたようだ。この二つの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__.fooC.fooと等価で、クラスオブジェクトからメソッドを取得する式である。このようにクラスから取り出されたメソッドunbound methodオブジェクトと呼ばれ、bound methodと同様に毎回生成される。ちなみにこのunbound methodメソッドはPython3で捨てられた要らない子なので、特に解説はしない。

というわけで、二番目のprint

print c.foo is c.__class__.foo

Falseとなる。c.fooc.__class__.fooは、どちらも毎回新しいinstancemethod型のオブジェクトを生成するので、is演算子で一致することはあり得ないのである。

id(c.foo) == id(c.__class__.foo)

次に1番目のprint文を見てみよう。二つのinstancemethodオブジェクト c.fooc.__class__.fooid を取得し、比較する式だ。それぞれ別々のオブジェクトの id を比較しているのだから当然Falseとなる………だろうか?

id()の説明をちょっと読んでみよう。

id(object)
オブジェクトの ``識別値'' を返します。この値は整数 (または長整数) で、このオブジェクトの有効期間は一意かつ定数であることが保証されています。 オブジェクトの有効期間が重ならない 2 つのオブジェクトは同じ id() 値を持つかもしれません。 (実装に関する注釈: この値はオブジェクトのアドレスです。)

http://www.python.jp/doc/2.5/lib/built-in-funcs.html

オブジェクトのidは一意だが、それはオブジェクトの有効期間に限るのだ。id(c.foo) と書くと、

  1. c から fooメソッドを取得
  2. 取得したメソッドを引数としてid()を呼び出す
  3. 取得したメソッドを解放

という順序で動作する。

次のid(c.__class__.foo)でも同様に

  1. c__class__ から fooメソッドを取得
  2. 取得したメソッドを引数としてid()を呼び出す
  3. 取得したメソッドを解放

となるが、ここでc.__class__.fooオブジェクトのidを取得するときには、既にc.fooメソッドは解放されてしまっているため、id が一意になるとは保証されないのである。bound methodunbound 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)

のように書かなければならない。