Python | OOP:クラス変数

Python Python
スポンサーリンク
  1. 概要(クラス変数は「全インスタンスで共有する設定・定数」)
  2. 基本の使い方(定義・アクセス・上書きのルール)
    1. 定義とアクセスの最短型
    2. インスタンスによる「影」を作る(上書きではなく“シャドーイング”)
  3. 重要ポイントの深掘り(可変データ・初期値戦略・継承とMRO)
    1. 可変データをクラス変数にしない(全員が同じものを共有する)
    2. 「デフォルトはクラス変数、カスタムはインスタンス」で柔軟に
    3. 継承時の振る舞い(派生ごとに設定を持てる)
  4. クラス変数を安全に変更する(classメソッド・コンストラクタ外の管理)
    1. クラス全体の設定変更はクラスメソッドで一箇所に
    2. インスタンス生成時に「その時点のクラス変数」を取り込む
  5. 実践パターン(Web/APIの設定・定数・計測)
    1. 共有設定(ベースURL・ユーザーエージェントなど)
    2. 共有の統計・計測(注意:可変は競合・連動の管理が必要)
  6. 補助トピック(dataclassのClassVar・読み取り専用の表現)
    1. dataclassで「インスタンスに含めない共有」を宣言する
    2. 「定数」扱いのクラス変数は命名と運用で守る
  7. よくあるつまずきと回避(意図せぬ共有・影の発生・更新の入口)
    1. 意図せぬ共有(可変をクラス変数に)
    2. “影”で混乱(インスタンス側に同名を作ってしまう)
    3. 更新の入口がバラバラ
  8. 例題(クラス変数で“系統別デフォルト”を持つAPIクライアント)
    1. サブクラスごとにデフォルトを切り替える
  9. まとめ(「共有はクラス、個別はインスタンス」。可変は持たせない)

概要(クラス変数は「全インスタンスで共有する設定・定数」)

クラス変数は、クラスに属する値で、作られたすべてのインスタンスから共有されます。対してインスタンス変数は各個体専用。初心者がまず押さえるべきは「何を共有し、何を個別にするかの線引き」「可変データをクラス変数に置かない」「アクセスと上書きの仕組み(ClassName.attr/instance.attrの違い)」です。


基本の使い方(定義・アクセス・上書きのルール)

定義とアクセスの最短型

class Counter:
    unit = "回"     # クラス変数(共有)

    def __init__(self):
        self.n = 0  # インスタンス変数(個別)

c1 = Counter()
c2 = Counter()
print(Counter.unit)  # "回"(クラスから参照)
print(c1.unit, c2.unit)  # "回" "回"(インスタンスからも見える)
Python
  • クラス変数はクラス名から参照するのが基本形。インスタンスからも見えますが、更新のつもりならクラス側に対して行うべきです。

インスタンスによる「影」を作る(上書きではなく“シャドーイング”)

c1.unit = "回数"      # c1のインスタンス辞書に新規属性を作る
print(c1.unit, c2.unit, Counter.unit)  # "回数" "回" "回"
del c1.unit
print(c1.unit)        # "回"(クラス変数が再び見える)
Python
  • インスタンスから代入すると“その個体だけの属性”が作られ、クラス変数を隠します。共有値を変えたいなら「Counter.unit = …」を使います。

重要ポイントの深掘り(可変データ・初期値戦略・継承とMRO)

可変データをクラス変数にしない(全員が同じものを共有する)

class Bag:
    items = []  # NG: 全インスタンスが同じリストを共有してしまう

b1 = Bag(); b2 = Bag()
b1.items.append("a")
print(b2.items)  # ["a"] ← 連動してしまう

class SafeBag:
    def __init__(self):
        self.items = []  # OK: 個別のリスト
Python
  • list/dict/setのような可変は必ずインスタンス側へ。クラス変数は不変な定数や軽い設定値に限定すると事故が減ります。

「デフォルトはクラス変数、カスタムはインスタンス」で柔軟に

class Config:
    timeout = 5.0  # 共有のデフォルト

cfg = Config()
print(cfg.timeout)  # 5.0(クラス変数が見える)
cfg.timeout = 10.0  # 個別に上書き(インスタンス属性を作る)
print(cfg.timeout)  # 10.0
Python
  • 共通の初期値はクラス変数、個別調整はインスタンス属性に。不要になったら del で消せばクラスの値が再び見えます。

継承時の振る舞い(派生ごとに設定を持てる)

class Base:
    kind = "base"

class Child(Base):
    kind = "child"

print(Base.kind, Child.kind)  # "base" "child"
b, c = Base(), Child()
print(b.kind, c.kind)         # "base" "child"
Python
  • サブクラスで同名を再定義すれば、その系統のインスタンス全体に別設定を適用できます(メソッド解決順序MROに従い、より近いクラスが優先)。

クラス変数を安全に変更する(classメソッド・コンストラクタ外の管理)

クラス全体の設定変更はクラスメソッドで一箇所に

class ApiClient:
    default_timeout = 5.0

    @classmethod
    def set_default_timeout(cls, v: float):
        if v <= 0:
            raise ValueError("timeoutは正の数")
        cls.default_timeout = v

    def __init__(self, base_url: str, timeout: float | None = None):
        self.base_url = base_url
        self.timeout = timeout if timeout is not None else type(self).default_timeout
Python
  • 共有設定の入口を用意すると、検証ロジックを統一でき、思わぬ変更を防げます。

インスタンス生成時に「その時点のクラス変数」を取り込む

  • initでクラス変数を読み、インスタンス属性へコピーすれば「生成後は個別に変更」できます。共有設定の“スナップショット”として扱いたいときに有効です。

実践パターン(Web/APIの設定・定数・計測)

共有設定(ベースURL・ユーザーエージェントなど)

class Client:
    base_url = "https://api.example.com"  # 共有のデフォルト
    user_agent = "MyApp/1.0"

    def __init__(self, api_key: str, timeout: float | None = None):
        import requests
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "User-Agent": type(self).user_agent,
        })
        self.timeout = timeout or 5.0

    def get(self, path: str) -> dict:
        r = self.session.get(f"{type(self).base_url}{path}", timeout=self.timeout)
        r.raise_for_status()
        return r.json()
Python
  • 共有値は type(self).attr で参照すると、サブクラスでの上書きにも自然に対応できます。

共有の統計・計測(注意:可変は競合・連動の管理が必要)

class Stats:
    total_calls = 0  # 不変ではないが、単純なカウンタはクラス変数で共有可能

    @classmethod
    def inc(cls):
        cls.total_calls += 1

    @classmethod
    def get(cls) -> int:
        return cls.total_calls
Python
  • マルチスレッドではロックが必要。頻繁に更新される可変共有は、専用の管理クラスや外部ストアへ寄せる方が安全です。

補助トピック(dataclassのClassVar・読み取り専用の表現)

dataclassで「インスタンスに含めない共有」を宣言する

from dataclasses import dataclass
from typing import ClassVar

@dataclass
class Settings:
    retries: int
    DEFAULT_TIMEOUT: ClassVar[float] = 5.0  # インスタンスフィールドには入らない共有値
Python
  • ClassVarで「これはクラス変数(フィールド化しない)」を示せます。型チェックや自動生成の誤動作を防ぎます。

「定数」扱いのクラス変数は命名と運用で守る

  • 大文字スネーク(API_BASE_URL、MAX_RETRY)にし、コード規約で「変更禁止」を徹底。言語仕様上の強制はないため、運用ルールとレビューで守るのが現実的です。

よくあるつまずきと回避(意図せぬ共有・影の発生・更新の入口)

意図せぬ共有(可変をクラス変数に)

  • 現象: あるインスタンスの変更が他インスタンスにも反映される。
  • 回避: 可変は必ずインスタンス属性に。共有したいなら不変化(タプル)や専用管理を検討。

“影”で混乱(インスタンス側に同名を作ってしまう)

  • 現象: クラス側を書き換えても、特定のインスタンスだけ古い値のまま。
  • 回避: 共有値の変更は「ClassName.attr」で行う。インスタンス側の同名属性がないか確認し、あれば削除して整合を取る。

更新の入口がバラバラ

  • 現象: どこで変えているか追跡不能、予期せぬ挙動。
  • 回避: クラスメソッドや設定クラスに変更を集約し、検証とログを一箇所に。

例題(クラス変数で“系統別デフォルト”を持つAPIクライアント)

サブクラスごとにデフォルトを切り替える

import requests

class BaseClient:
    base_url = "https://api.example.com"
    timeout_default = 5.0

    def __init__(self, api_key: str, timeout: float | None = None):
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})
        self.timeout = timeout if timeout is not None else type(self).timeout_default

    def get(self, path: str) -> dict:
        r = self.session.get(f"{type(self).base_url}{path}", timeout=self.timeout)
        r.raise_for_status()
        return r.json()

class EUClient(BaseClient):
    base_url = "https://eu.api.example.com"
    timeout_default = 7.0

class USClient(BaseClient):
    base_url = "https://us.api.example.com"

eu = EUClient("KEY")
us = USClient("KEY")
print(eu.timeout, us.timeout)  # 7.0 5.0
Python
  • クラス変数で「系統別のデフォルト」を表現し、type(self)経由で継承の上書きに自然対応。個別のインスタンスでは引数で上書きできます。

まとめ(「共有はクラス、個別はインスタンス」。可変は持たせない)

クラス変数は「全インスタンスに共通の定数・設定」を置く場所。アクセスはクラス名(またはtype(self))から行い、共有値を変更したいときはクラス側で。インスタンスから代入すると“影”ができて混乱するので注意。可変データはクラス変数に置かず、インスタンス属性へ。継承で系統別デフォルトを持たせ、変更の入口はクラスメソッドに集約する。これだけで、初心者でも「壊れない・読みやすい」クラス変数設計が自然にできるようになります。

タイトルとURLをコピーしました