突然ですが、次のコードは何を出力するでしょうか?
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
ayumi.kokugo = 90
Exam.kokugo = -1
print(Exam.kokugo, ayumi.kokugo, genta.kokugo)
-1 90 -1
と答えた方は本記事は読まなくても大丈夫です(が良ければ神の目線でご覧ください)。
そうでない方はぜひ読んで行って下さい。
Pythonのクラス変数とインスタンス変数
Pythonは好きな言語の1つですが、Pythonでよく分からないと言われることにクラス変数とインスタンス変数があります。
この2つが合わさった挙動は直感に反する場合があり、新たなPython開発者を混乱させることがあります。
この記事ではPythonのクラス変数とインスタンス変数で混乱してしまう内容について具体例を通して確認し、「クラスやインスタンスが持つ変数の定義はこう書こう」というのを簡単に例示してみます。
想定外の値変化
ある小学校で、テストの結果を管理するプログラムを作りたいとします。
次のサンプルコードを見てください。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
print(ayumi.kokugo, genta.kokugo) # -> 0 0
ayumi.kokugo = 90
print(ayumi.kokugo, genta.kokugo) # -> 90 0
genta.kokugo = 30
print(ayumi.kokugo, genta.kokugo) # -> 90 30
Exam
は小テストの点数を管理するクラスです。ここでは国語の点数だけ定義しています。
国語の点数をセットする前は0が表示され、セットした後はセットした値が出力されます。何も問題ないように見えます。
ではちょっと変なことをしてみましょう。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
print(ayumi.kokugo, genta.kokugo) # -> 0 0
ayumi.kokugo = 90
print(ayumi.kokugo, genta.kokugo) # -> 90 0 ...①
Exam.kokugo = -1 # +++
print(ayumi.kokugo, genta.kokugo) # -> 90 -1 ...②
genta.kokugo = 30
print(ayumi.kokugo, genta.kokugo) # -> 90 30 ...③
国語のデフォルト値を途中で変えてみました。
②のprint文を見てください。元太くんの国語は-1
が出力されています。しかし元太くんの国語には何も代入していません。
ここに違和感を覚える方は多いのではないでしょうか。90 0
が表示されるはずだと思ってしまいます。
何が起こっているのでしょうか?
インスタンス生成時の変数はどうなっているのか
コードの最初から、1つ1つ追っていきたいと思います。
まずインスタンスを作った直後のメモリ状態のイメージを見てみましょう(以降メモリイメージと呼びます)。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
この状態では、Examクラスやayumi, gentaインスタンスのメモリイメージはどういう状態でしょうか。
下図に近い状態を想像するのではないでしょうか。
残念ながらこれは誤りです。
ayumi
, genta
インスタンスを生成した段階ではayumi.kokugo
とgenta.kokugo
というインスタンス変数はありません。
では、インスタンス生成直後のayumi.kokugo
, genta.kokugo
は何故アクセスできるのでしょうか?また、何を指しているのでしょうか?
実はこの時点では、この2つの変数名はクラス変数であるExam.kokugo
と同じメモリを指しています。
この時点でayumi.kokugo
はインスタンス変数にアクセスするような記述ですが、クラス変数にアクセスしています。
試しにExam.kokugo
, ayumi.kokugo
, genta.kokugo
のIDを見てみます。確かに、同じIDが出力されます。
注) ID出力について サンプルコードにおける補足 を本記事の最後に載せています。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
print(id(Exam.kokugo), id(ayumi.kokugo), id(genta.kokugo))
# -> 4347660720 4347660720 4347660720
この3つは、同じ場所を指しているようです。
では、それを踏まえてayumi.kokugo
に90点を代入してみましょう。
分かりやすいようにExam.kokugo
も一緒に出力します。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
print(id(Exam.kokugo), id(ayumi.kokugo), id(genta.kokugo))
ayumi.kokugo = 90 # +++
print(Exam.kokugo, ayumi.kokugo, genta.kokugo) # -> 0 90 0
出力について変に思うかもしれません。
「同じ場所を示すのならば、ayumi.kokugo = 90
を実行するとExam.kokugo
, genta.kokugo
も90になって、90 90 90
になるはずじゃないか?」
代入文でインスタンス変数が作られる
実は、Pythonではayumi.kokugo = 90
のようにインスタンス変数への代入文の中で、初めてインスタンス変数が作られます。 そして、以降ayumi.kokugo
はそのインスタンス変数を指します。
ここが分かりにくいポイントです。
ではIDを出して確認してみましょう。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
print(id(Exam.kokugo), id(ayumi.kokugo), id(genta.kokugo))
# -> 4312050096 4312050096 4312050096
ayumi.kokugo = 90
print(Exam.kokugo, ayumi.kokugo, genta.kokugo) # -> 0 90 0
print(id(Exam.kokugo), id(ayumi.kokugo), id(genta.kokugo))
# -> 4312050096 4312052976 4312050096
ayumi.kokugo
のIDが変わっていますね。
メモリイメージは次のようになります。
ayumiに新しくメモリ領域が確保され、ayumi.kokugo
はそちらを指します。
ここでクラス変数Exam.kokugo
とは別の場所を指すayumi.kokugo
というインスタンス変数が作られるのですね。
では続けて、Exam.kokugo
に-1
を代入してみましょう。
これまでの話を踏まえると、何を出力されるのかイメージしやすいかと思います。
(読みづらいのでIDの出力はここでやめます)
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
ayumi.kokugo = 90
print(Exam.kokugo, ayumi.kokugo, genta.kokugo) # -> 0 90 0
Exam.kokugo = -1 # +++
print(Exam.kokugo, ayumi.kokugo, genta.kokugo) # -> -1 90 -1
Exam.kokugo = -1
を追加しました。Exam.kokugo
が-1
を出力するのは当然として、genta.kokugo
は同じ場所を指しているので、genta.kokugo
も-1
を出力します。
メモリイメージを見てみましょう。
では元太くんの点数も設定してみます。
これは歩美ちゃんの時と同じです。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
ayumi.kokugo = 90
print(Exam.kokugo, ayumi.kokugo, genta.kokugo) # -> 0 90 0
Exam.kokugo = -1
print(Exam.kokugo, ayumi.kokugo, genta.kokugo) # -> -1 90 -1
genta.kokugo = 30
print(Exam.kokugo, ayumi.kokugo, genta.kokugo) # -> -1, 90 30
メモリイメージを見てみます。
新しくインスタンス変数のメモリが確保され、genta.kokugo
はそちらを指すようになりましたね。
Class直下の記述はクラス変数
改めて何が分かりにくいのかを考えてみます。
前提として、Pythonではclass
直下に記述した変数はクラス変数となります。
class Exam:
kokugo = 0
kokugoはクラス変数です。C++やJavaでは、いわゆるクラス変数を定義したい場合static
をつけますが、Pythonでは付けません。class直下の変数定義は全てクラス変数です。
これについては「Pythonではそうなんだ」と受け取るだけかと思います。
では何が分かりにくくさせるのかというと、まずPythonではインスタンス変数参照の記述でクラス変数を参照できます。
class Exam:
kokugo = 0
ayumi = Exam()
print(ayumi.kokugo) # -> 0 インスタンス変数風だが、クラス変数への参照
ただ、これはJavaやRubyでも同じです。
これに加えPythonでは、代入文でインスタンス変数が生成される言語仕様というのがあります。したがってayumi.kokugo = 90
をすると、さっきまでayumi.kokugo
はクラス変数を指していたのに以後ayumi.kokugo
はインスタンス変数を指すようになります。この挙動が混乱を招いてしまっているのだと思います。
改めて、この記事の最初のコードを見てみます。
class Exam:
kokugo = 0
ayumi = Exam()
genta = Exam()
print(ayumi.kokugo, genta.kokugo) # クラス変数参照 クラス変数参照
ayumi.kokugo = 90
print(ayumi.kokugo, genta.kokugo) # インスタンス変数参照 クラス変数参照
genta.kokugo = 30
print(ayumi.kokugo, genta.kokugo) # インスタンス変数参照 インスタンス変数参照
問題ないように見えていた挙動でしたが、代入文によりクラス変数からインスタンス変数へ参照が変わっています。多くの場合、これは意図したものではないと思います。
インスタンス変数は__init__
へ
では例題の歩美ちゃん元太くんの小テスト結果のようなことをしたければどうすればいいのでしょうか。
繰り返しになりますが、代入文でインスタンス変数が生成されます。
インスタンス変数を定義したい場合は、インスタンス生成時に実行される__init__
で代入文を記述しましょう。
class Exam:
def __init__(self):
self.kokugo = 0
ayumi = Exam()
print(ayumi.kokugo) # -> 0 ayumi.kokugoはインスタンス変数
パイソニスタには見慣れた記述ですが、改めて見るとなるほどと思います。
Exam()
でインスタンス生成時に__init__
が実行されるので、self.kokugo = 0
でインスタンス変数kokugo
が生成され、これをayumi
に代入するのでayumi.kokugo
がayumi
インスタンスのkokugo
変数になるわけですね。
下記のようにdataclass
を用いても良いです。
ここでは細かい内容は省きますが、dataclass
は上記コードの__init__
を生成してくれるシンタックスシュガーです。
from dataclasses import dataclass
@dataclass
class Exam:
kokugo = 0
ayumi = Exam()
print(ayumi.kokugo) # -> 0 ayumi.kokugoはインスタンス変数
より直感に近い記述になりますね。
まとめ
Pythonのクラス変数参照について、分かりにくさの原因となる挙動を具体例を通して確認し、なぜ分かりにくいのかのポイントをまとめました。
Pythonのクラス変数とインスタンス変数について理解が深まれば幸いです。
補足) サンプルコードにおけるID確認
Pythonでは数値リテラルは最適化され、値が同じだと同じIDを返します。
a = 0
b = 0
print(id(a), id(b)) # -> 4340976048 4340976048
a = 999999
b = 999999
print(id(a), id(b)) # -> 4487693488 4487693488
なので、サンプルコードでExam.kokugo
, ayumi.kokugo
, genta.kokugo
のIDを確認してますが、同じ値では同じIDになるため、インスタンス変数とクラス変数の参照の切り替わりを確認する方法としては実は不適です。
分かりやすさのため記事上では数値リテラルにしてますが、ご自分で正しく参照の変更を確認するにはkokugo
をクラスでWrapしてあげてください。
class Score:
def __init__(self, num: int):
self.num = num
class Exam:
kokugo = Score(0)
ayumi = Exam()
genta = Exam()
print(id(Exam.kokugo), id(ayumi.kokugo), id(genta.kokugo))
# -> 4454425072 4454425072 4454425072
ayumi.kokugo = Score(90)
print(id(Exam.kokugo), id(ayumi.kokugo), id(genta.kokugo))
# -> 4454425072 4454639264 4454425072
genta.kokugo = Score(90)
print(id(Exam.kokugo), id(ayumi.kokugo), id(genta.kokugo))
# -> 4454425072 4454639264 4454639504
参考
- Python3 公式ドキュメント
- 9.3.5. クラスとインスタンス変数