モダンPython中級 #1 dataclassと__slots__
モダンPython基礎 シリーズを終えたら、ここから一段階上に入ります。中級シリーズは 基礎で軽く触れたツールを本格的に扱う 7本構成です。
- #1 dataclass と
__slots__← 今回 - #2 typing 本格 — Generic、Protocol、TypedDict、Literal
- #3 コンテキストマネージャー (
with、contextlib) - #4 イテラブル / ジェネレータ /
yield from - #5 デコレータパターン
- #6 パターンマッチングの深さ
- #7 非同期入門 (asyncio)
最初のテーマは データを入れるクラスを短く書くツール — @dataclass と、そこにメモリ節約オプションを加える __slots__ です。
データクラスが解いてくれる問題 #
次のようなクラスを書いた経験があれば、何が面倒かすぐにわかります。
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 はパスワードのようにログに出てはいけないフィールドによく使います。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 入力の検証なら 基礎 #2 で少し見た Pydantic が圧倒的に優位です。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% の高速化 くらいが一般的です (オブジェクトのサイズとインタプリタのバージョンによって異なる)。
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が__init__/__repr__/__eq__を自動生成- オプション:
frozen(イミュータブル、hashable)、kw_only(位置引数を遮断)、order(ソート)、slots(メモリ) - ミュータブルなデフォルト値は
field(default_factory=list)で field(repr=False, compare=False, init=False)でフィールド単位の制御__post_init__で生成後の後処理__slots__— インスタンスあたりの dict オーバーヘッドを除去、メモリ・速度を節約dataclass(slots=True)が最も短いスロット使用法- 強い検証や JSON シリアライズは dataclass より Pydantic が正解
次回 (#2 typing 本格) では、型システムの強力なツール — Generic、Protocol、TypedDict、Literal を扱います。基礎で押さえた型ヒントの次のステップです。