Python講座 / インスタンス属性とクラス属性

インスタンス属性は個々のオブジェクトに属し、クラス属性はクラスから参照できる共通の値です。特に変更可能なリストをクラス属性にすると、複数のインスタンスが同じリストを変更する場合があります。

状態を置く場所を選ぶ

各台帳の支出一覧は、その台帳専用のself.itemsとして__init__の中で作ります。クラス本体にitems = []と書き、個々のインスタンスに同名の属性を作らない場合、同じリストを共有します。一方で共通の通貨表示のような変更しない既定値には、クラス属性を使えます。

属性の参照ではインスタンス側の値が優先されます。b.currencyへ代入しても、通常はLedger.currency自体を変更せず、bに同名の属性を作って既定値を隠します。

名前の共有とdataclassのデフォルト値は、dataclassでデータを表すで扱う規則とつながります。

台帳ごとの一覧を実行で確認する

attributes.pyへ保存し、python attributes.pyを実行します。初期化の引数ownerは、使用可能な台帳を作るために必要な情報です。

class Ledger:
    currency = "KRW"

    def __init__(self, owner):
        self.owner = owner
        self.items = []

    def add(self, amount):
        self.items.append(amount)

a = Ledger("A")
b = Ledger("B")
a.add(3500)
print(a.items, b.items)
b.currency = "USD"
print(a.currency, b.currency, Ledger.currency)

予想結果は次のとおりです。

[3500] []
KRW USD KRW

aへの追加はbへ影響せず、bの通貨変更もaやクラスの値へ影響しません。

渡されたリストの共有にも注意する

self.items = []は呼び出しごとに新しいリストを作ります。しかし初期化の引数で受け取ったリストをそのまま属性へ保存すると、呼び出し元と同じリストを参照します。独立した一覧が必要ならコピーする契約を決めます。

浅いコピーでは、入れ子のリストや辞書は共有されたままです。参照とコピーの違いも確認してください。またLedger.currencyを後から変更しても、既に独自のcurrencyを持つbの値はそのままです。通貨が各台帳の重要な設定なら、初期化の明示的な引数にする方が分かりやすい場合があります。

共有による不具合を再現して戻す

練習用コピーでself.items = []を削除し、クラス本体へitems = []を移します。aへ追加した後でb.itemsにも値が見えることを確認してください。その後、必ず元のインスタンス属性の形へ戻し、独立性を再確認します。

共有状態が意図的な設計なら、誰がいつ作り、いつ破棄し、同時アクセスをどう扱うかまで決めます。クラス属性を偶然の全体保存先として使うと、テスト同士や利用者同士の状態が混ざりやすくなります。

クラス本体に置いたリストはクラスを通じて共有されます。個別のリストが必要なら__init__内で self.items = [] を作ります。次の例で、一回のappendが他のインスタンスにも見える設計と、個別に保持する設計を比較します。

全体を attributes.py に保存し、同じ練習フォルダーで python attributes.py を実行します。

class Shared:
    items = []
class Separate:
    def __init__(self):
        self.items = []
a, b = Shared(), Shared()
a.items.append(1)
print(a.items, b.items)
x, y = Separate(), Separate()
x.items.append(1)
print(x.items, y.items)

予想結果:

[1] [1]
[1] []

関連する公式リファレンス