Hatena::ブログ(Diary)

logging.info(self)

2011-04-19

Pythonのクラス変数とインスタンス変数の違い

| 23:12 | Pythonのクラス変数とインスタンス変数の違いを含むブックマーク

動機

Pythonの言語リファレンスとか他の人のコードみてたらクラス変数インスタンス変数の違いがよくわからなくなったのでメモ。

というか、Ponsukeのプログラミング日記インスパイアされて書きました。


今まで

自分が最初に触ったオブジェクト指向言語Javaで、そっちの概念を引きずったままPythonをやり始めたので当然クラス変数インスタンス変数は別々に宣言するものなんだろという考えていた。

つまり

class Hoge(object):
    FOO = int()

とすればクラス変数FOOが宣言されたことになりインスタンス変数は宣言されてないものだと思ってた。

そしてPythonインスタンス変数を使うには

class Hoge(object):
    def __init__(self):
        self.FOO = int()

として、クラス変数インスタンス変数両方使うには上の2つを組み合わせ、

class Hoge(object):
    FOO = int()
    def __init__(self):
        self.FOO = int()

とやるべきという認識でいた。

しかしこれが間違っていてずっとそれを使っていたのでここに記しておこうと思った。


言語仕様

Pythonの言語リファレンスを見てみると

クラス定義内で定義された変数はクラス変数です; クラス変数は全てのインスタンス間で共有されます。

とされている。

つまり

class Hoge(object):
    FOO = int()

このコードはクラス変数で間違いない。

さらに、

インスタンス変数を作成するには、メソッドの中で self.name = value でセットできます。

となっていて、

class Hoge(object):
    def __init__(self):
        self.FOO = int()

これで作成しているはず。

クラス変数インスタンス変数も “self.name” 表記でアクセスすることができます。この表記でアクセスする場合、インスタンス変数は同名のクラス変数を隠蔽します。

これはコードでいうと

class Hoge(object):
    FOO = int()
    def __init__(self):
        self.FOO = int()
    def get_foo(self):
        return self.FOO

になって、「selfのスコープから見えるFOOがインスタンス変数のFOOだからクラス変数のFOOにはアクセスできないよ」と言ってるんだろう。


実際どうなのよ?

先人の見解を参考にしつつ自分で書いたコードが以下。

いつものようにunittestで書いてるけどassertTrueにしてみた。

若干こっちの方が見やすい気がする。

本当はdoctestの方がassert文とかなくて見やすいからいいんだろうけど、エディタシンタックスカラーが効かなくてコードが書きにくいのでunittestにしてます。

ちなみに実行環境は

Ubuntu10.10(32bit)

Python2.6.6

です。

#! python
# coding:utf-8

"""クラス変数とインスタンス変数

Pythonでのクラス変数とインスタンス変数を理解する為のコード片
"""

import unittest

class C(object):
    """クラス変数FOOとインスタンス変数FOOを持つクラス"""

    FOO = int()

    def get_foo(self):
        """インスタンス変数FOOの取得"""
        return self.FOO

    @classmethod
    def get_class_foo1(cls):
        """クラスメソッドを使っての取得"""
        return cls.FOO

    def get_class_foo2(self):
        """インスタンスメソッドだがクラス変数にアクセスしているもの"""
        return C.FOO

class D(object):
    """クラス変数FOOは持たないがインスタンス変数FOOを持つクラス"""

    def __init__(self):
        """コンストラクタでインスタンス変数FOOを作る"""
        self.FOO = int()

    def get_foo(self):
        """インスタンス変数FOOの取得"""
        return self.FOO


class TestClassVarAndInstanceVar(unittest.TestCase):

    def setUp(self):
        C.FOO = 10  # テスト毎にクラス変数を初期化

    def test_modify_instance_var_only(self):
        c1 = C()
        # インスタンス変数へセットしているはず
        c1.FOO = 100

        self.assertTrue(hasattr(c1, "FOO"))             # インスタンスにFOOはある
        self.assertTrue(c1.FOO == 100)                  # これはインスタンス変数
        self.assertTrue(c1.get_foo() == 100)            # メソッド経由でインスタンス変数取得

    def test_class_var_access(self):
        c1 = C()
        self.assertTrue(hasattr(C, "FOO"))              # クラス変数にもFOOはある
        self.assertTrue(C.FOO == 10)                    # これはクラス変数
        self.assertTrue(c1.get_class_foo1() == 10)      # クラスメソッドでクラス変数取得その1
        self.assertTrue(c1.get_class_foo2() == 10)      # クラスメソッドでクラス変数取得その2
        self.assertTrue(C.get_class_foo1() == 10)       # クラスメソッドでクラス変数取得その3
        self.assertRaises(TypeError, C.get_class_foo2)  # インスタンスメソッドの呼び出しはできない(unbound)

    def test_modify_class_var_and_instnce_var(self):
        c1 = C()
        c2 = C()

        C.FOO = 20
        self.assertTrue(c1.get_class_foo1() == 20)      # クラス変数は20であり

        c1_cls_FOO = c1.get_class_foo1()
        c2_cls_FOO = c2.get_class_foo1()
        self.assertTrue(c1_cls_FOO == c2_cls_FOO)       # クラス間で共有されている

        c1.FOO = 100
        self.assertTrue(c1.get_foo() == 100)            # インスタンス変数のFOOを変更しているはず
        self.assertTrue(c1.get_class_foo1() != 100)     # 故にクラス変数は100ではなく
        self.assertTrue(c1.get_class_foo1() == 20)      # 20であるはず

    def test_class_var_not_exists(self):
        d = D()
        d.FOO = 100

        self.assertTrue(hasattr(d, "FOO"))              # インスタンス変数FOOはあるが
        self.assertTrue(not hasattr(D, "FOO"))          # クラス変数の変数FOOは存在しない


if __name__ == '__main__':
    unittest.main()

実行結果はもちろんオールグリーンですよ。

....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
 
OK

結論

ま、私の認識が間違っていましたってだけなんだけどね。