クラス変数とインスタンス変数の混同に注意

WEB で property 周りの記事を調べていたら、次のようなコードで、x や y をインスタンス変数だと解説している例を見つけた。

class C(object):
    x = 10
    y = 20

確かに、

>>> c = C()
>>> c.x        # ... (1)
10
>>> c.x = 30   # ... (2)
>>> c.x        # ... (3)
30

となって一見正しいように見える。しかし、実際に起きているのは次の現象。

  • (1) ではインスタンス変数 c.x が存在しないので、クラス変数 C.x の値 10 が読まれる。
  • (2) で、インスタンス変数 c.x が作られ、そこに 30 が代入される。
  • (3) ではインスタンス変数 c.x が存在するのでその値 30 が読まれる。クラス変数 C.x は隠される。

実際にこうなっていることは、id() 関数で変数のアドレスを確認すればはっきりする。

>>> c = C()
>>> id(C.x)
134872388
>>> id(c.x)
134872388
>>> c.x = 30
>>> id(c.x)
134872148

c.x に値を代入する前は c.x と C.x は同じアドレスを示しているのに対し、代入後は c.x が別アドレスになっているのがわかる。
意図したようにインスタンス変数を使うためには、メソッドの中で self の属性として定義する必要がある。

class C(object):
    def __init__(self):
        self.x = 10
        self.y = 20

property 周りの解説に書かれていたので、もしかすると、property の次の書式に引きずられたのかもしれない。

class C(object):
    ...
    x = property(get_x, set_x, del_x)
    z = 30
    def y(self):
        ...

たしかに x と z は書式上、似ている。C のインスタンス c に対して c.x でインスタンス変数に (間接的に) アクセスできるので、z もインスタンス変数と思ってしまいがちかもしれない。しかし、z は単なる int 型の変数であるのに対し、x は property オブジェクトになるのが大きな違いである。def でメソッドを定義すると関数オブジェクトを作って C の名前空間の辞書に y という名前で登録するのと同じように、property オブジェクトを作って C の名前空間の辞書に x という名前で登録している。