目次
8 章

dataclass と __slots__

データを集めるクラスを短く安全に作るための @dataclass の全オプション — frozen、kw_only、field() と、メモリ節約ツールの __slots__ までを整理します。

2部 コードの構造化 の最初の章です。1部の7章を終えて関数 / コレクション / 例外 / モジュールまでは押さえましたが、「形が決まったオブジェクト」を短く安全に表現する方法はまだ見ていません。本章では @dataclass__slots__ でその問題を整理します。

本章の dataclass は本書全体でよく出会います。第9章 typing 本格 — Generic、Protocol、TypedDict、Literal の Protocol と対になり、4部 FastAPI の Pydantic モデルは事実上 dataclass の拡張です(第24章 Pydantic v2 深掘り で明示的に比較します)。

データクラスが解いてくれる問題 #

次のようなクラスを書いた経験があれば、何が面倒かすぐに分かります。

🚫 直接書いたクラス — 長くて反復的
class User:
    def __init__(self, id: int, name: str, age: int):
        self.id = id
        self.name = name
        self.age = age

    def __repr__(self) -> str:
        return f"User(id={self.id!r}, name={self.name!r}, age={self.age!r})"

    def __eq__(self, other) -> bool:
        if not isinstance(other, User):
            return NotImplemented
        return (self.id, self.name, self.age) == (other.id, other.name, other.age)

3つのフィールドのために __init____repr____eq__ を直接書きました。フィールドを1つ追加すると3ヶ所を一緒に直さなければなりません。

@dataclass がこれを解いてくれます。

✅ @dataclass — 同じこと
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    age: int

これで終わりです。__init____repr____eq__ が自動で作られます。

動作
u = User(id=1, name="カーティス", age=30)
print(u)
# User(id=1, name='カーティス', age=30)

print(u == User(id=1, name="カーティス", age=30))   # True

型ヒント(id: int など)がそのまま フィールド定義 になります。一か所でデータの形を宣言すれば、動作は自動です。

@dataclass のオプション — よく使うもの #

オプション付きの形
from dataclasses import dataclass

@dataclass(frozen=True, kw_only=True, slots=True)
class User:
    id: int
    name: str
    age: int = 0

各オプションの意味:

オプションデフォルト意味
frozenFalseTrue ならイミュータブル — 生成後にフィールド変更不可
kw_onlyFalseTrue なら全フィールドを keyword-only で受け取る
slotsFalseTrue なら自動で __slots__ を追加 (3.10+)
eqTrue__eq__ を自動生成
orderFalseTrue なら <> などの比較演算子を自動生成
reprTrue__repr__ を自動生成
initTrue__init__ を自動生成

frozen=True — イミュータブルなオブジェクト #

frozen
@dataclass(frozen=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
p.x = 3.0    # ✗ FrozenInstanceError

イミュータブルだと良い点:

  • 辞書のキー、set の要素として使える(自動で hashable になる)
  • 意図しない変更が防げる — 関数に渡したあと誰かが直してバグになる事故を防止
  • マルチスレッドで安全(race condition なし)

ドメインモデルで「このデータは作られたら変わらない」という場所に本当によく合います。

kw_only=True — 位置引数を禁止 #

kw_only
@dataclass(kw_only=True)
class User:
    id: int
    name: str
    age: int = 0

u = User(id=1, name="カーティス")          # OK
u = User(1, "カーティス")                  # ✗ TypeError

第5章 関数の引数パターン の keyword-only と同じ効果です。フィールドが多くなるほど User(1, "カーティス", 30, True, "admin") のような呼び出しは読みにくいですが、kw_only=True が強制的に止めてくれます。新しいデータクラスは デフォルトでオンにすることをおすすめ します。

slots=True — メモリと速度 #

slots
@dataclass(slots=True)
class Point:
    x: float
    y: float

このオプションの意味は本章の後半で詳しく扱います。一行要約は 「インスタンスをより軽く速くする」 です。

比較可能にする — order=True #

order
@dataclass(order=True)
class Score:
    value: int
    name: str

scores = [Score(80, "B"), Score(95, "A"), Score(70, "C")]
scores.sort()    # 自動で動作
print(scores)    # [Score(70, 'C'), Score(80, 'B'), Score(95, 'A')]

<<=>>=フィールドの順番にタプルのように 比較されます。最初のフィールドが同じなら次のフィールドに進みます。順序比較が意味を持つ場所(スコア、時間、座標)に有用です。

field() — フィールド単位の細かい設定 #

デフォルト値が単純な値ではないとき、またはより細かいオプションが必要なときに field() を使います。

落とし穴 — ミュータブルなデフォルト値は直接書いてはいけない #

🚫 エラー
@dataclass
class User:
    name: str
    tags: list[str] = []   # ✗ ValueError
# mutable default <class 'list'> for field tags is not allowed

第5章 で見たミュータブルなデフォルト値の落とし穴です。dataclass が親切に捕まえてくれます。default_factory を使います。

✅ default_factory
from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    tags: list[str] = field(default_factory=list)

各インスタンスごとに新しい空のリストが作られます。

field() の他のオプション #

field オプション
from dataclasses import dataclass, field

@dataclass
class User:
    id: int
    # 1. デフォルト値
    role: str = "member"
    # 2. ミュータブルなデフォルト値
    tags: list[str] = field(default_factory=list)
    # 3. repr/eq から除外
    password: str = field(repr=False, compare=False, default="")
    # 4. init から除外 — 生成後に別の場所で埋める
    created_at: float = field(init=False, default=0.0)
    # 5. メタデータ
    score: int = field(default=0, metadata={"max": 100})

repr=False はパスワードのようにログに出てはいけないフィールドによく使います(第31章 logging と観測性 で PII / secret のログ出力防止パターンを改めて扱います)。compare=False は同一性判定に影響を与えないように — たとえば created_at が違っても同じユーザーとして扱われるように。

__post_init__ — 生成後の後処理 #

__init__ が自動生成されるので直接書きにくいのですが、生成直後に追加処理をしたいとき。

__post_init__
from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)

    def __post_init__(self):
        self.area = self.width * self.height

r = Rectangle(3, 4)
print(r.area)   # 12.0

init=False でコンストラクタから除外したフィールドを __post_init__ で計算して埋めるパターンがよくあります。

dataclass が合わない場所 #

万能ではありません。次のような場所には別のツールを見てください。

状況より合うツール
検証が強く必要(メールアドレスの形式、長さ制限など)Pydantic
JSON 変換が頻繁(シリアライズ / デシリアライズ)Pydantic、attrs、msgspec
継承 + 動作が多い通常のクラス
名前付きタプルで十分NamedTuple
どうせ dict でいいTypedDict

API 入力の検証なら Pydantic の方が適しています。第24章 Pydantic v2 深掘り で詳しく扱います。dataclass は「内部のデータモデル」用と考えればよいです。

__slots__ — メモリと速度 #

ここから slots=True の正体に入ります。

普通のインスタンス — __dict__ で動作 #

Python のオブジェクトは基本的に 属性を dict に保存 します。

dict で動作
class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

p = Point(1.0, 2.0)
print(p.__dict__)
# {'x': 1.0, 'y': 2.0}

p.z = 3.0    # 新しい属性を自由に追加可能
print(p.__dict__)
# {'x': 1.0, 'y': 2.0, 'z': 3.0}

長所: 非常に柔軟。短所: 属性ひとつごとに dict のオーバーヘッド が毎回かかります。オブジェクトを数百万個作るとメモリが大きく増えます。

__slots__ — あらかじめ決めた属性のみ #

__slots__
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

p = Point(1.0, 2.0)
p.z = 3.0    # ✗ AttributeError: 'Point' object has no attribute 'z'

__slots__ を定義すると:

  • __dict__ が作られない — メモリが減る
  • 属性追加不可 — 定義された名前のみ使用
  • 属性アクセスが少し速い — dict 検索ではなく直接スロットアクセス

数値で見るとインスタンスあたり 40〜50% のメモリ節約、属性アクセス 10〜25% の高速化 くらいが一般的です(オブジェクトのサイズとインタプリタのバージョンによって異なります)。正確なメモリ測定ツールは第21章 パフォーマンス — cProfile、py-spy、メモリプロファイリング で道具を使って直接見ます。

dataclass(slots=True) が最も楽な使い方 #

__slots__ を直接書くと、フィールド名を2ヶ所(typing 宣言 + __slots__)書かなければなりません。dataclass(slots=True) が自動で処理してくれます。

自動スロット生成
from dataclasses import dataclass

@dataclass(slots=True)
class Point:
    x: float
    y: float

裏で起きていることは上で直接書いたものと同じです。一行で済むので、あえて使わない理由はありません。

__slots__ を使うときの注意 #

万能ではありません。

1) 多重継承に制約 #

__slots__ が定義されたクラス同士で多重継承すると衝突します。一般的な単一継承だけなら問題ありません。

2) 弱参照 — weakref ができない #

デフォルトの __slots__ は weakref をサポートしません。必要なら:

weakref サポート
class Node:
    __slots__ = ("data", "__weakref__")

dataclass(slots=True, weakref_slot=True) も可能です (3.11+)。

3) クラス変数との衝突に注意 #

🚫 衝突
class Bad:
    __slots__ = ("x",)
    x = 0    # ✗ ValueError — 同じ名前のクラス変数とスロット

4) 動的な属性が追加できない #

プラグイン / モッキングなどでオブジェクトに一時的な属性を付けるパターンは壊れます。普通は問題になりませんが、ライブラリを作るときはユーザーがそうする可能性を考えてください。

いつスロットをオンにするか #

状況スロット
インスタンスを 数万〜数百万個 作るデータモデル(座標、グラフのノードなど)✅ 必ず
イミュータブル性を強く 保ちたい — 任意の属性追加を遮断
一般的なドメインオブジェクト、インスタンスが多くない⭕ オンにしても問題なし(とりあえずオンに)
メタプログラミング / 動的属性 / 多重継承が活発❌ オフ、または慎重に

迷ったら dataclass(slots=True) をデフォルトに するのがモダン Python のいつもの答えです。

練習問題 #

  1. @dataclass(frozen=True, kw_only=True, slots=True)Address(country: str, city: str, postal_code: str) を定義してください。Address("KR", "Seoul", "06000") の呼び出しが TypeError になるか (kw_only)、同じ値で作った2つのインスタンスが == か、hash(addr) が動作するか (frozen → hashable)、addr.city = "Busan"FrozenInstanceError かを確認します。
  2. @dataclassCart クラスを作ってください。items: list[str] フィールドはミュータブルなデフォルト値なので field(default_factory=list) が必要です。total: float フィールドは init から除外し (field(init=False, default=0.0))、__post_init__items の個数 × 1000 として計算して埋めます。
  3. @dataclass 1万個と @dataclass(slots=True) 1万個のインスタンスをそれぞれ作ったうえで、sys.getsizeof(instance) + sys.getsizeof(instance.__dict__) (slots 側は __dict__ がないので try / except) でインスタンスあたりのメモリを比較してみてください。正確なメモリ測定ツールは第21章 パフォーマンス で改めて扱います。

一行まとめ: @dataclass 一行が __init__/__repr__/__eq__ を自動生成する。よく使うオプションは frozen (イミュータブル・hashable)、kw_only (呼び出しの可読性)、slots (メモリ)、order (ソート)。ミュータブルなデフォルト値は field(default_factory=...)、生成後の後処理は __post_init__。検証 / シリアライズが必要なら dataclass ではなく Pydantic。

次の章 #

次の 第9章 typing 本格 — Generic、Protocol、TypedDict、Literal では、型システムの強力な道具を扱います。本章の dataclass と第9章の Protocol が合わさると「構造的型付け」の核心パターンが完成します。

X