9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonのクラス変数とインスタンス変数の区別ついてますか?これでもう間違えない!

Last updated at Posted at 2023-08-04

突然ですが、次のコードは何を出力するでしょうか?

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のクラス変数とインスタンス変数で混乱してしまう内容について具体例を通して確認し、「クラスやインスタンスが持つ変数の定義はこう書こう」というのを簡単に例示してみます。

想定外の値変化

ある小学校で、テストの結果を管理するプログラムを作りたいとします。
次のサンプルコードを見てください。

test.py
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が表示され、セットした後はセットした値が出力されます。何も問題ないように見えます。
ではちょっと変なことをしてみましょう。

test.py
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つ追っていきたいと思います。
まずインスタンスを作った直後のメモリ状態のイメージを見てみましょう(以降メモリイメージと呼びます)。

test.py
class Exam:
    kokugo = 0

ayumi = Exam()
genta = Exam()

この状態では、Examクラスやayumi, gentaインスタンスのメモリイメージはどういう状態でしょうか。
下図に近い状態を想像するのではないでしょうか。

memory1_1.png

残念ながらこれは誤りです。
ayumi, gentaインスタンスを生成した段階ではayumi.kokugogenta.kokugoというインスタンス変数はありません。
memory1_2.png

では、インスタンス生成直後のayumi.kokugo, genta.kokugoは何故アクセスできるのでしょうか?また、何を指しているのでしょうか?
実はこの時点では、この2つの変数名はクラス変数であるExam.kokugoと同じメモリを指しています。
この時点でayumi.kokugoはインスタンス変数にアクセスするような記述ですが、クラス変数にアクセスしています。

memory1_3.png

試しにExam.kokugo, ayumi.kokugo, genta.kokugoのIDを見てみます。確かに、同じIDが出力されます。

注) ID出力について サンプルコードにおける補足 を本記事の最後に載せています。

test.py
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も一緒に出力します。

test.py
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を出して確認してみましょう。

test.py
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が変わっていますね。
メモリイメージは次のようになります。

memory1_4.png

ayumiに新しくメモリ領域が確保され、ayumi.kokugoはそちらを指します。
ここでクラス変数Exam.kokugoとは別の場所を指すayumi.kokugoというインスタンス変数が作られるのですね。

では続けて、Exam.kokugo-1を代入してみましょう。
これまでの話を踏まえると、何を出力されるのかイメージしやすいかと思います。
(読みづらいのでIDの出力はここでやめます)

test.py
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を出力します。
メモリイメージを見てみましょう。

memory1_5.png

では元太くんの点数も設定してみます。
これは歩美ちゃんの時と同じです。

test.py
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

メモリイメージを見てみます。

memory1_6.png

新しくインスタンス変数のメモリが確保され、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はインスタンス変数を指すようになります。この挙動が混乱を招いてしまっているのだと思います。

改めて、この記事の最初のコードを見てみます。

test.py
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.kokugoayumiインスタンスの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. クラスとインスタンス変数

9
7
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?