Cover image for Class vs Instance Variables: The Mutation Trap That Catches Experienced Developers

Ameer Abdullah

クラス変数 vs インスタンス変数:経験豊富な開発者をも捕らえるミューテーションの罠

共有されミュータブルなクラス属性は、上級Python面接で頻出する非自明な振る舞いを生み出します


class Student:
    grades = []

    def add_grade(self, grade):
        self.grades.append(grade)

alice = Student()
bob = Student()

alice.add_grade(90)bob.add_grade(85)

print(alice.grades)
print(bob.grades)

Enter fullscreen mode Exit fullscreen mode

このコードは何を出力すると思いますか?

出力:

[90, 85]
[90, 85]

Enter fullscreen mode Exit fullscreen mode

両方のStudentが同じgradesリストを共有しています。これがクラス変数のミューテーションの罠です。


なぜこうなるのか

クラス変数はインスタンスではなくクラスオブジェクト自体に定義されます。 self.grades にアクセスすると、Pythonはまずインスタンスの __dict__ を確認します。何も見つからなければ、クラス内の属性を検索します。 alicebob のどちらも Student クラス上の同じリストオブジェクトを見つけます。

.append() はその共有リストオブジェクトをミューテートします。すべてのインスタンスが変更を認識します。


修正方法

class Student:
    def __init__(self):
        self.grades = []  # インスタンス変数。各インスタンスごとに新しく作成される

    def add_grade(self, grade):
        self.grades.append(grade)

alice = Student()
bob = Student()

alice.add_grade(90)bob.add_grade(85)

print(alice.grades)
print(bob.grades)

Enter fullscreen mode Exit fullscreen mode

出力:

[90]
[85]

Enter fullscreen mode Exit fullscreen mode

リストの初期化を __init__ に移動することで、各インスタンスごとに新しいリストが作成されます。


インスタンス変数のシャドーイング

class Counter:
    count = 0

    def increment(self):
        self.count += 1

c1 = Counter()
c2 = Counter()

c1.increment()
c1.increment()
c2.increment()

print(Counter.count)
print(c1.count)
print(c2.count)

Enter fullscreen mode Exit fullscreen mode

出力:

0
2
1

Enter fullscreen mode Exit fullscreen mode

self.count += 1self.count = self.count + 1 と等価です。 self.count の読み取りはクラス変数 (0) を見つけます。代入は self 上に新しいインスタンス変数を作成し、クラス変数をシャドーイングします。クラス変数は常に 0 のままです。

これはミュータブルなクラス属性の場合とは異なります。整数はイミュータブルであるため、 += はクラスレベルのオブジェクトをミューテートするのではなく、新しいオブジェクトを作成してインスタンスにバインドします。


クラス変数が適切な場合

クラス変数は以下の場合に適しています:

  • すべてのインスタンスで共有される定数

  • すべてのインスタンスに適用される設定値

  • クラス全体の状態を真に追跡するカウンタ(慎重な管理が必要)

class DatabaseConnection:
    MAX_CONNECTIONS = 10  # 定数 — クラス変数として適切
    active_connections = 0  # クラス全体のカウンタ — 慎重に管理すれば適切

    def __init__(self):
        if DatabaseConnection.active_connections >= DatabaseConnection.MAX_CONNECTIONS:
            raise RuntimeError("Connection limit reached")
        DatabaseConnection.active_connections += 1

Enter fullscreen mode Exit fullscreen mode

クラス変数を変更する場合は、意図を明確にし、意図しないインスタンス変数の作成を避けるため、 self ではなくクラス名を通じて行うべきであることに注意してください。


PyCodeIt 2.0 が正式にリリースされました!

究極のPython面接用サンドボックスを一から再構築しました。コンピュータサイエンスの学生、データサイエンスの卒業生、または技術面接に合格しようとしている開発者にとって、Pythonのコードトレースは非常にテストされながらもほとんど練習されないスキルです。PyCodeItはまさにそのギャップを埋めるために構築されており、最新のアップデートですべてが変わります。

pycodeit.com でクラス変数のトレース問題を練習してください。