モダンPython中級 #1 dataclassと__slots__

読了 7分

モダンPython基礎 シリーズを終えたら、ここから一段階上に入ります。中級シリーズは 基礎で軽く触れたツールを本格的に扱う 7本構成です。

  • #1 dataclass と __slots__ ← 今回
  • #2 typing 本格 — Generic、Protocol、TypedDict、Literal
  • #3 コンテキストマネージャー (withcontextlib)
  • #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 がこれを解いてくれます。

✅ @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 はパスワードのようにログに出てはいけないフィールドによく使います。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 入力の検証なら 基礎 #2 で少し見た Pydantic が圧倒的に優位です。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% の高速化 くらいが一般的です (オブジェクトのサイズとインタプリタのバージョンによって異なる)。

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 のいつもの答えです。

まとめ #

今回まとめたツール:

  • @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 を扱います。基礎で押さえた型ヒントの次のステップです。

X