Python – OOP Part 3. クラス変数(Class Variable)

読了 8分

前回の講座ではオブジェクトの概念を学びました。そして、クラスの定義、インスタンスの生成、selfを使ったインスタンスメソッドとインスタンス変数の使用などを試してみました。今回の講座では、インスタンス変数とは少し異なる概念であるクラス変数について学んでいきます。

クラス変数とは? インスタンス変数が人の名前のように、それぞれのインスタンスが持っている固有のデータだとすれば、クラス変数はある団体の団体名のように、同じクラスから作られたすべてのインスタンスが共有するデータです。

ある会社が従業員の年俸を毎年1回引き上げてくれるのですが、特異なことに全従業員の年俸を同じ昇給率で引き上げてくれるそうです。今年は会社の売上が高く、全従業員の年俸を10%ずつ上げてくれるそうです。(夢のような話ですね 笑) このとき、すべての従業員に適用される共通の昇給率がクラス変数として使用できる良い例です。例題を見ながら説明していきます。

お好きなディレクトリに「oop_3.py」という名前のファイルを作って、下のコードを保存して実行してください。

oop_3.py
class Employee:
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * 1.1)  #1 年俸を10%引き上げます。

emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 既存の年俸')
print(emp_1.pay)  # 既存の年俸

print('\n# 昇給率の適用')
emp_1.apply_raise()  # 昇給率の適用

print('\n# 上がった年俸')
print(emp_1.pay)  # 上がった年俸
実行結果
# 既存の年俸
50000

# 昇給率の適用

# 上がった年俸
55000

12行目のコード #1 で1.1、つまり10%の昇給率を適用して年俸が引き上げられたことが分かります。しかし、上のコードは昇給率をハードコーディングした良くない例です。もし1.1の昇給率を一箇所だけでなく複数の箇所で使うとしたらどうでしょうか?昇給率が変わるたびにすべての箇所の数字を変更しなければならず、もし修正で漏れる部分があると大きな問題が発生する可能性があるわけです。そのため、最も理想的な方法はハードコーディングをせず、変数に値を代入し、必要な箇所では変数の値を参照し、変更が必要なときには変数の値だけを修正する方法です。

クラス変数を使ってコードを修正してみましょう。

oop_3.py
class Employee:
raise_amount = 1.1  # 1 クラス変数の定義

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * raise_amount)  # 2 クラス変数の使用


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 既存の年俸')
print(emp_1.pay)  # 既存の年俸

print('\n# 昇給率の適用')
emp_1.apply_raise()  # 昇給率の適用

print('\n# 上がった年俸')
print(emp_1.pay)  # 上がった年俸
実行結果
# 既存の年俸
50000

# 昇給率の適用
Traceback (most recent call last):
File "C:\Users\CURTIS\Dev\Python\5G\add_host\oop_3.py", line 24, in <module>
emp_1.apply_raise()  # 昇給率の適用
File "C:\Users\CURTIS\Dev\Python\5G\add_host\oop_3.py", line 14, in apply_raise
self.pay = int(self.pay * raise_amount)  # 2 クラス変数の使用
NameError: name 'raise_amount' is not defined

2行目のコード #1 でクラス変数を定義し、14行目のコード #2 で参照しました。おっと… ‘raise_amount’ は定義されていない名前だとして NameError が発生しました。なぜでしょうか?!? そう、クラス変数はクラスの名前空間に保存されているため、クラスを通じてアクセスしなければならないのです。コードを修正してみます。

oop_3.py
class Employee:
raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * Employee.raise_amount)  # 1 クラス Employee を使ってアクセス


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 既存の年俸')
print(emp_1.pay)  # 既存の年俸

print('\n# 昇給率の適用')
emp_1.apply_raise()  # 昇給率の適用

print('\n# 上がった年俸')
print(emp_1.pay)  # 上がった年俸
実行結果
# 既存の年俸
50000

# 昇給率の適用

# 上がった年俸
55000

14行目のコード #1 でクラスである Employee を通じて raise_amount 変数にアクセスしてみました。おお、問題なく実行されました。

オブジェクトの名前空間 Employee の代わりに、インスタンスである self を通じても raise_amount にアクセスできるでしょうか?できないですよね…クラス変数なのに self でどうやって…一度やってみましょう。

oop_3.py
class Employee:
raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 インスタンスである self を使ってアクセス


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# 既存の年俸')
print(emp_1.pay)  # 既存の年俸

print('\n# 昇給率の適用')
emp_1.apply_raise()  # 昇給率の適用

print('\n# 上がった年俸')
print(emp_1.pay)  # 上がった年俸
実行結果
# 既存の年俸
50000

# 昇給率の適用

# 上がった年俸
55000

なんと、こんなことが…インスタンスを通じてもアクセスできました。なぜでしょう???Python は下の図のような形の名前空間というものを持っています。この名前空間はオブジェクトの名前を分けて管理しており、名前を探すときに「インスタンスの名前空間」→「クラスの名前空間」→「スーパークラスの名前空間」の順で探していきます。しかし、逆方向には探しません。つまり、子は親の名前空間を参照することができるが、親が子の名前空間を参照することはできないということです。上のコードのように「self.raise_amount」を使うと、Python は最初にインスタンスの名前空間で「raise_amount」という名前を探し、なければクラスの名前空間で探すのです。

Python オブジェクトの名前検索順序
Python オブジェクトの名前検索順序

__dict__ メソッドを使ってクラスとインスタンスの名前空間の中を覗いてみましょう。

oop_3.py
class Employee:
raise_amount = 1.1

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

print('# インスタンスの名前空間を参照')
print(emp_1.__dict__)

print('\n# クラスの名前空間を参照')
print(Employee.__dict__)
実行結果
# インスタンスの名前空間を参照
{'first': 'Sanghee', 'last': 'Lee', 'pay': 50000, 'email': 'sanghee.lee@schoolofweb.net'}

# クラスの名前空間を参照
{'__module__': '__main__', 'raise_amount': 1.1, '__init__': <function Employee.__init__ at 0x000001E37154F790>, 'full_name': <function Employee.full_name at 0x000001E37154F820>, 'apply_raise': <function Employee.apply_raise at 0x000
001E37154F8B0>, '__dict__': <attribute '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>, '__doc__': None}

上の結果を見ると分かるように、emp_1 インスタンスの名前空間には raise_amount がなく、Employee クラスオブジェクトの名前空間にだけ存在していることが分かります。下の例を見ると、もう少し理解できると思います。

oop_3.py
class SuperClass:
super_var = 'スーパー名前空間にある変数です。'


class MyClass(SuperClass):
class_var = 'クラスの名前空間にある変数です。'

    def __init__(self):
        self.instance_var = 'インスタンスの名前空間にある変数です。'


my_instance = MyClass()

# アクセス可能なケース
print('my_instance.instance_var')
print(my_instance.instance_var)
print('\nmy_instance.class_var')
print(my_instance.class_var)
print('\nmy_instance.super_var')
print(my_instance.super_var)
print('\nMyClass.class_var')
print(MyClass.class_var)
print('\nMyClass.super_var')
print(MyClass.super_var)
print('\nSuperClass.super_var')
print(SuperClass.super_var)
print('-' * 30)

# アクセス不可能なケース
try:
print(SuperClass.class_var)
except:
print('SuperClass.class_var')
print('class_varを見つけることができません...')

try:
print(MyClass.instance_var)
except:
print('\nMyClass.instance_var')
print('instance_varを見つけることができません...')
実行結果
my_instance.instance_var
インスタンスの名前空間にある変数です。

my_instance.class_var
クラスの名前空間にある変数です。

my_instance.super_var
スーパー名前空間にある変数です。

MyClass.class_var
クラスの名前空間にある変数です。

MyClass.super_var
スーパー名前空間にある変数です。

SuperClass.super_var
スーパー名前空間にある変数です。
------------------------------
SuperClass.class_var
class_varを見つけることができません...

MyClass.instance_var
instance_varを見つけることができません...

これである程度理解されたと思います。それでは、もう一度最初の例題に戻りましょう。

これまで毎年同じ昇給率をすべての従業員に適用していた会社の方針が変わりました。今年の優秀社員に選ばれた ‘Sanghee Lee’ にだけ特別な昇給率の20%を適用するそうです。 😭 ありがとうございます… 笑 こういう場合にはクラス変数を使えないのでしょうか?このような場合にはクラス変数とインスタンス変数を両方とも使えばよいです。下のコードをご覧ください。

oop_3.py
class Employee:
raise_amount = 1.1  # クラス変数を使ってすべての従業員に適用

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 インスタンス変数から参照します。


emp_1 = Employee('Sanghee', 'Lee', 50000)
emp_2 = Employee('Minjung', 'Kim', 60000)

emp_1.raise_amount = 1.2  # インスタンス変数を使って特別昇給率を適用

print('# emp_1 年俸20%引き上げ')
print(emp_1.pay)
emp_1.apply_raise()
print(emp_1.pay)
print('# emp_2 年俸10%引き上げ')
print(emp_2.pay)
emp_2.apply_raise()
print(emp_2.pay)
実行結果
# emp_1 年俸20%引き上げ
50000
60000
# emp_2 年俸10%引き上げ
60000
66000

上のように14行目のコード #1 で self を使ってインスタンス変数から参照し始めると、emp_1 はインスタンス変数の名前空間に raise_amount があるため参照され、emp_2 はないので自動的にクラス変数である raise_amount を参照することになります。

それでは今度は、クラス変数を使うのに適した別の例を挙げてみます。

会社の従業員数を管理しなければならないと考えてみましょう。このような場合には各従業員がこのデータを持っている必要はなく、ある一箇所にデータを保存して参照するのが最も良いでしょう?下のコードをご覧ください。

oop_3.py
class Employee:

    raise_amount = 1.1
    num_of_emps = 0  # 1 クラス変数の定義

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first.lower() + '.' + last.lower() + '@schoolofweb.net'

        Employee.num_of_emps += 1  # 2 インスタンスが生成されるたびに1ずつ増加

    def __del__(self):
        Employee.num_of_emps -= 1  # 3 インスタンスが削除されるたびに1ずつ減少

    def full_name(self):
        return '{} {}'.format(self.first, self.last)

    def apply_raise(self):
        self.pay = int(self.pay * self.raise_amount)  # 1 インスタンス変数から参照します。


print(Employee.num_of_emps)  # 最初の従業員数
emp_1 = Employee('Sanghee', 'Lee', 50000)  # 従業員1名入社
emp_2 = Employee('Minjung', 'Kim', 60000)  # 従業員1名入社
print(Employee.num_of_emps)  # 従業員数の確認

del emp_1  # 従業員1名退社
del emp_2  # 従業員1名退社
print(Employee.num_of_emps)  # 従業員数の確認
実行結果
0
2
0

21行目のコード #1 でクラス変数 num_of_emps を定義し、インスタンスが作られるたびに実行される __init__ メソッドの中(#2)で num_of_emps の値を1ずつ増加させました。そして #3 では deconstructor である __del__ を使って、インスタンスが削除されるたびに num_of_emps の値を1ずつ減少させてみました。このようにクラス変数を使うことで、インスタンス変数では管理しにくいデータを簡単に管理することができるのです。ある意味では、一般の関数で使うグローバル変数(global variable)に似た概念です。

いかがですか?これで、いつインスタンス変数を使えばよく、いつクラス変数を使うべきか分かるようになりましたか?ところで、ある方々はこんな考えをお持ちかもしれません。「クラス変数があるのなら、クラスメソッドもあるのではないか?」と。そうです。インスタンスメソッド以外にも「クラスメソッド」と「スタティックメソッド」というものがあるのですが、これらについては次の講座で扱っていきます。

お疲れさまでした~!

X