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 がこれを解いてくれます。
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各オプションの意味:
| オプション | デフォルト | 意味 |
|---|---|---|
frozen | False | True ならイミュータブル — 生成後にフィールド変更不可 |
kw_only | False | True なら全フィールドを keyword-only で受け取る |
slots | False | True なら自動で __slots__ を追加 (3.10+) |
eq | True | __eq__ を自動生成 |
order | False | True なら <、> などの比較演算子を自動生成 |
repr | True | __repr__ を自動生成 |
init | True | __init__ を自動生成 |
frozen=True — イミュータブルなオブジェクト
#
@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 — 位置引数を禁止
#
@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 — メモリと速度
#
@dataclass(slots=True)
class Point:
x: float
y: floatこのオプションの意味は本章の後半で詳しく扱います。一行要約は 「インスタンスをより軽く速くする」 です。
比較可能にする — order=True
#
@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 を使います。
from dataclasses import dataclass, field
@dataclass
class User:
name: str
tags: list[str] = field(default_factory=list)各インスタンスごとに新しい空のリストが作られます。
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__ が自動生成されるので直接書きにくいのですが、生成直後に追加処理をしたいとき。
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.0init=False でコンストラクタから除外したフィールドを __post_init__ で計算して埋めるパターンがよくあります。
dataclass が合わない場所
#
万能ではありません。次のような場所には別のツールを見てください。
| 状況 | より合うツール |
|---|---|
| 検証が強く必要(メールアドレスの形式、長さ制限など) | Pydantic |
| JSON 変換が頻繁(シリアライズ / デシリアライズ) | Pydantic、attrs、msgspec |
| 継承 + 動作が多い | 通常のクラス |
| 名前付きタプルで十分 | NamedTuple |
| どうせ dict でいい | TypedDict |
API 入力の検証なら Pydantic の方が適しています。第24章 Pydantic v2 深掘り で詳しく扱います。dataclass は「内部のデータモデル」用と考えればよいです。
__slots__ — メモリと速度
#
ここから slots=True の正体に入ります。
普通のインスタンス — __dict__ で動作
#
Python のオブジェクトは基本的に 属性を 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__ — あらかじめ決めた属性のみ
#
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 をサポートしません。必要なら:
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 のいつもの答えです。
練習問題 #
@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かを確認します。@dataclassでCartクラスを作ってください。items: list[str]フィールドはミュータブルなデフォルト値なのでfield(default_factory=list)が必要です。total: floatフィールドは init から除外し (field(init=False, default=0.0))、__post_init__でitemsの個数 × 1000 として計算して埋めます。@dataclass1万個と@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 が合わさると「構造的型付け」の核心パターンが完成します。