Pythonのクラス変数とインスタンス変数:違いと正しい更新方法(シャドーイングに注意)

Pythonのクラス内で扱う変数には、大きく分けて**「クラス変数」「インスタンス変数」**の2種類があります。これらは定義する場所やスコープ(有効範囲)、データの保持のされ方が根本的に異なります。

特に、「インスタンスからクラス変数を書き換えようとした場合」の挙動は、Python初心者が最も陥りやすい罠の一つです。

この記事では、クラス変数の基本的な使い方と、インスタンス変数との違い、そして意図しないバグを防ぐための正しい更新方法について解説します。

目次

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

クラス変数

  • 定義場所: クラスブロックの直下(メソッドの外側)。
  • 特徴: そのクラスから生成されたすべてのインスタンスで共有される値。
  • 用途: 定数、設定値、生成されたインスタンスの総数カウントなど。

インスタンス変数

  • 定義場所: __init__ メソッドなどの内部で self.変数名 として定義。
  • 特徴: 個々のインスタンスごとに独立した値。
  • 用途: 名前、年齢、個別のステータスなど。

具体的なコード例:ユーザー管理クラス

例として、Webサービスのユーザーを表すクラスを作成します。 サイト名(全員共通)を「クラス変数」で、ユーザー名(個別)を「インスタンス変数」で管理し、さらにユーザーの総数をカウントします。

class SiteUser:
    """
    ユーザー管理クラス
    """
    # --- クラス変数 ---
    # 全インスタンスで共有される
    site_name = "Python Study Portal"
    total_user_count = 0

    def __init__(self, username):
        """コンストラクタ"""
        # --- インスタンス変数 ---
        # インスタンスごとに独立している
        self.username = username
        
        # インスタンス生成時に、クラス変数を更新(カウントアップ)
        SiteUser.total_user_count += 1

    def show_info(self):
        print(f"サイト: {SiteUser.site_name} | ユーザー: {self.username}")

# 1. クラス変数はインスタンス化しなくても参照可能
print(f"初期ユーザー数: {SiteUser.total_user_count}")

# 2. インスタンスの生成
user1 = SiteUser("Tanaka")
user2 = SiteUser("Suzuki")

# 3. 各インスタンスの情報を表示
user1.show_info()
user2.show_info()

# 4. クラス変数が共有されているか確認
# どのインスタンスから見ても、クラス変数の値は同じになっている
print(f"user1から見た総数: {user1.total_user_count}")
print(f"user2から見た総数: {user2.total_user_count}")
print(f"クラスから見た総数: {SiteUser.total_user_count}")

実行結果:

初期ユーザー数: 0
サイト: Python Study Portal | ユーザー: Tanaka
サイト: Python Study Portal | ユーザー: Suzuki
user1から見た総数: 2
user2から見た総数: 2
クラスから見た総数: 2

user1user2 を生成したことで、共有されている total_user_count2 に増えていることがわかります。

重要な注意点:インスタンス経由での代入(シャドーイング)

クラス変数を扱う上で最も注意すべき点は、**「インスタンス経由でクラス変数に値を代入してはいけない」**ということです。

もし user1.site_name = "New Site" のように代入を行うと、クラス変数が更新されるのではなく、user1 というインスタンスの中に、たまたま同じ名前の新しいインスタンス変数が作成されてしまいます。これを変数の「シャドーイング(隠蔽)」と呼びます。

Python

# クラス変数の現在の値
print(f"[変更前] Class: {SiteUser.site_name}, user1: {user1.site_name}, user2: {user2.site_name}")

# 【悪い例】インスタンス経由で代入
# クラス変数を変更したつもりだが...
user1.site_name = "Renamed Portal"

# 確認
print(f"[変更後] Class: {SiteUser.site_name}, user1: {user1.site_name}, user2: {user2.site_name}")

実行結果:

[変更前] Class: Python Study Portal, user1: Python Study Portal, user2: Python Study Portal
[変更後] Class: Python Study Portal, user1: Renamed Portal, user2: Python Study Portal

解説

  • user1.site_name: "Renamed Portal" に変わりました。これは user1 に新しく作られたインスタンス変数です。
  • SiteUser.site_name: 元のままです。クラス変数は更新されていません。
  • user2.site_name: 元のままです。クラス変数を参照し続けています。

これにより、user1 だけがデータ同期から外れてしまうバグが発生します。

正しい更新方法

クラス変数を更新したい場合は、必ずクラス名を使ってアクセスします。

# 【良い例】クラス名を使って代入
SiteUser.site_name = "Official Python Site"

# 全てに反映される
print(f"Class: {SiteUser.site_name}")
print(f"user2: {user2.site_name}")
# (注意: user1 は先ほどの操作でインスタンス変数を持ってしまったため、そちらが優先表示されます)

まとめ

  • クラス変数: ClassName.var でアクセス。全インスタンスで共有したいデータに使用します。
  • インスタンス変数: self.var でアクセス。個別のデータに使用します。
  • 更新のルール: クラス変数を更新する際は、必ず ClassName.var = 値 と記述します。self.var = 値instance.var = 値 と書くと、インスタンス変数が新規作成され、クラス変数が隠蔽されてしまいます。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次