こんなツイート(エックス)を見かけて気になったので、検証も兼ねてメモを残してみる。
オブジェクトのクラスを定義するとき、2種類の変数を使い分けることができる。それが「クラス変数」と「インスタンス変数」である。
クラス変数というのは、クラスに紐づく変数である。よくクラスとインスタンスは、設計図と製品に例えられるが、クラス変数というのは設計図自体に紐づく変数であるから、個々の製品に共通する変数ということになる。
これに対して、インスタンス変数というのは、各インスタンスに紐づく変数であるから、製品Aと製品A'が同じ設計図から作られていたとしても、インスタンス変数は別々の値を持つことになる。
これをふまえて最初のツイートのプログラムを見てみると、「インスタンス変数と想定して書いたつもりがクラス変数の動作をしてしまった」という感じに読み取れる。
class MyClass:
my_list = []
a = MyClass()
b = MyClass()
a.my_list.append(1)
print(b.my_list)
実際に colab なんかで実行してみると、確かに b の my_list に [1] の値が入っていることがわかる。 my_list
はクラス変数という扱いになるので、 a.my_list.append(1)
で更新されたら、 b.my_list
も同じ値を参照するために [1] が出力される。
これを本来の意図(aのmy_listとbのmy_listには違う値が入る)通りに修正したいなら、Pythonでは次のような実装にする必要がある。
class FixedMyClass:
def __init__(self):
self.my_list = []
a = FixedMyClass()
b = FixedMyClass()
a.my_list.append(1)
print(b.my_list)
コンストラクタの中で self.my_list
に渡してあげることで、 a.my_list
と b.my_list
には異なる値が入る。
こういうのはその都度、変数のスコープを意識しておくと、落とし穴にハマることは減る。気がする。でもPythonって結構よく分からん挙動するんだよなー。
ChatGPT先生に聞いてみた
っていう記事を書いた後に、「こういうのってChatGPT先生に聞いたらなんとかなるのでは?」と思って聞いてみたらこんな回答が。(以下Chat先生の回答)
ユーザーの指摘する問題点は、Pythonクラスで定義されたクラス変数の振る舞いについてです。Pythonでクラス定義時にリストなどの変数を宣言すると、その変数はそのクラスの全てのインスタンス間で共有されます。これは、このリストがひとつだけ作成され、その後の全ての新規インスタンスはそれと同じリストを参照することで挙動します。
class MyClass:
my_list = [] # 全てのインスタンスで共有されるリスト
a = MyClass() # MyClassの新規インスタンスを生成
b = MyClass() # MyClassの新規インスタンスを生成
a.my_list.append(1) # a.my_list に 1 を追加
print(b.my_list) # b.my_list を表示
上記のコードでは、インスタンスaの my_list に1を追加しました。しかし、その後インスタンスbの my_list を表示させると、このリストも1を含む結果が表示されます。これは my_list がクラス全体で共有されているためです。この挙動は初めて見ると驚きかもしれませんが、それはPythonのクラス level 変数についての知識がないと、予期せぬ結果をもたらす可能性があるからです。
そうではなく、インスタンスごとに固有のリストを持つ方法もあります。そのためには、リストをクラスのコンストラクタ( __init__ メソッド)で初期化する必要があります。
class MyClass:
def __init__(self):
self.my_list = [] # 各インスタンス固有のリスト
これなら、aとbがそれぞれ独立したリスト my_list を持つことになり、お互いから影響を受けることはありません。このように、Pythonの特定の特徴を理解していなければ、誤解を生むことがあることがわかります。そのため、プログラミングを学ぶ際は、基礎となる理論や仕組みの理解を深めることが大切です。
いいね。記事かく時間がもったいなかったね。ああーー、課題やるか。