0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0224(2025/08/25)Pythonクラス定義のTips①

Posted at

Pythonクラス定義のTips① — dataclass・eq・__post_init__を実務目線で

「dataclassは“宣言した事実”から安全な初期化と比較を自動化し、__post_init__は“仕上げ”、__eq__は“意味としての等しさ”を約束するフックである。」


1. dataclassの使い方 — “データの事実”を宣言し、自動生成に任せる

@dataclass は、フィールド宣言から __init____repr____eq__・(必要なら)__hash__・順序比較などを安全に自動生成する仕組みです。自分で __init__ を書き始める前に「本当に手で書く必要があるか?」を一度疑ってみると、だいたい不要です。

1.1 まずは最小の例

from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    active: bool = True
  • 生成される __init__(id: int, name: str, active: bool = True)宣言順で代入。
  • eq=True(既定)なので全フィールド比較__eq__ が自動生成。
  • repr=True(既定)で見やすい __repr__ も自動。

1.2 フィールドの役割を明示する

from dataclasses import dataclass, field

@dataclass(slots=True, frozen=True)
class Point:
    x: float
    y: float
    # 生成後に計算する派生値。比較・repr からも除外
    r: float = field(init=False, repr=False, compare=False)
  • slots=True で軽量かつ属性のタイポを防止。
  • frozen=True でイミュータブル。ハッシュ(__hash__)は安全に自動生成
  • field(init=False)__init__ の引数から外し、後で __post_init__ で設定する前提にできる。

1.3 dataclassオプション比較表(同じ階層の比較)

オプション 役割 既定 注意点
eq __eq__ を自動生成 True frozen=False かつ eq=True だと __hash__=None(非ハッシュ)。
order <, <=, >, >= 自動生成 False eq=True が前提。意味のある順序か要検討。
frozen イミュータブル化 False 変更不可。__post_init__object.__setattr__ 使用。
slots __slots__ 付与 False 動的属性不可。継承戦略も事前設計。
repr __repr__ 自動生成 True 機密情報の露出に注意。field(repr=False) で隠せる。
unsafe_hash 強制ハッシュ化 False 可変とハッシュの不整合に注意(原則避ける)。
kw_only 後続フィールドをキーワード専用に False APIの明確化に有効。

1.4 field(...) の主なパラメータ比較表

パラメータ 目的 典型値 ワンポイント
default 既定値 0, "" など ミュータブルは禁止。リスト等は default_factory
default_factory 生成時に関数呼び出し list, dict ミュータブルの鉄板。lambda: {} も可。
init __init__ 引数に含めるか True 派生値は False にして __post_init__ で設定。
repr __repr__ へ含めるか True 機密/冗長な値は False
compare 等価/順序に含めるか True キャッシュや一時値は False
hash ハッシュ対象に含めるか None 微調整が必要なときのみ。

2. __eq__ の使い方 — “意味としての等しさ”を定義する

__eq__a == b の規範を記述する場所です。NotImplemented を適切に返すことで、対称性(a == bb == a の整合)や他型との相互運用性を保てます。

2.1 最小実装

class Vec2:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __eq__(self, other):
        if isinstance(other, Vec2):
            return (self.x, self.y) == (other.x, other.y)
        return NotImplemented
  • 型が違う場合は NotImplemented を返す。False を返すと左右入れ替えが効かず、拡張性が落ちる。

2.2 浮動小数の“近さ”で比べる

import math

class Point:
    def __init__(self, x: float, y: float):
        self.x, self.y = x, y
    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (
            math.isclose(self.x, other.x, rel_tol=1e-9, abs_tol=0.0)
            and math.isclose(self.y, other.y, rel_tol=1e-9, abs_tol=0.0)
        )
  • 数値誤差を抱える領域では厳密一致ではなく近似一致を等価とするのが実務的。

2.3 __hash__ との整合

  • 原則a == b なら hash(a) == hash(b) を満たすこと。

  • dataclass では:

    • eq=True かつ frozen=True → 安全に __hash__ が自動。
    • eq=True かつ frozen=False → 既定で非ハッシュ(集合・辞書キーにできない)。
    • やむなく可変でもキー化したいなら unsafe_hash=True(更新で壊れる危険を理解した上で)。

2.4 比較表:__eq__ の返値と意味

返り値 意味 結果の流れ
True 等価 その場で True
False 非等価 その場で False左右入替の再試行なし
NotImplemented この型では判断不可 右オペランドの __eq__ を試し、なければ最終的に非等価

2.5 dataclass と __eq__ の住み分け

方針 向くケース 注意点
自動生成に任せる 「全フィールド一致=等価」 設定値のDTO、素直な値オブジェクト フィールドの compare=False を忘れない
自前実装する 近似一致/部分一致/別型対応 浮動小数、座標、IDのみで比較 NotImplemented を返す作法を徹底

3. __post_init__ の使い方 — 初期化後の“仕上げフック”

__post_init__ は dataclass の __init__全フィールド代入を終えた直後に呼ばれます。バリデーション、正規化、派生値の計算に最適です。順序が不安定だから必要なのではなく、「全部そろった後にやりたい処理」のための場所です。

3.1 代表的な用途

  • 入力の正規化:スペース除去、小文字化、Pathの展開など。
  • 整合チェック:複数フィールドの関係を検証(start < end など)。
  • 派生フィールドfield(init=False) とセットでキャッシュや要約を計算。
  • InitVar の処理:初期化時だけ受け取り、フィールドには保持しない値を扱う。
from dataclasses import dataclass, field, InitVar
import os

@dataclass
class Config:
    home: str
    # InitVar は __init__ 引数には現れるが、フィールドには保存されない
    env_home: InitVar[str | None] = None
    home_abs: str = field(init=False)

    def __post_init__(self, env_home):
        base = env_home or self.home
        self.home_abs = os.path.abspath(os.path.expanduser(base))

3.2 frozen=True と組み合わせる

from dataclasses import dataclass, field
import math

@dataclass(frozen=True)
class Circle:
    r: float
    area: float = field(init=False, repr=False)

    def __post_init__(self):
        if self.r <= 0:
            raise ValueError("r は正の数")
        # frozen でも後処理だけは object.__setattr__ で代入できる
        object.__setattr__(self, "area", math.pi * self.r ** 2)

3.3 比較表:__init__ に書く/__post_init__ に書く

書き場所 向く処理 メリット デメリット
__init__ を自前実装 dataclass の自動生成では表現できない複雑な初期化 あらゆる自由度 自動生成の恩恵を失いバグ源に。引数順・既定値管理が重い
__post_init__ 正規化、検証、派生値、InitVar 自動生成の利点を維持しつつ後処理を一括 代入は全フィールド完了後。frozen では object.__setattr__ が必要

4. 3者の連携パターン(実務で使う形)

4.1 値オブジェクト(不変×比較×派生キャッシュ)

from dataclasses import dataclass, field
import math

@dataclass(frozen=True, slots=True)
class Vec2:
    x: float
    y: float
    _len: float = field(init=False, repr=False, compare=False)

    def __post_init__(self):
        object.__setattr__(self, "_len", math.hypot(self.x, self.y))

    def __eq__(self, other):
        if not isinstance(other, Vec2):
            return NotImplemented
        # 浮動小数の近似比較
        return math.isclose(self.x, other.x, rel_tol=1e-9) and \
               math.isclose(self.y, other.y, rel_tol=1e-9)
  • compare=False でキャッシュを比較対象外に。
  • frozen=True なのでハッシュ可能、集合や辞書のキーに安全に使える。

4.2 IDベース等価+表示名は無関係

from dataclasses import dataclass, field

@dataclass(eq=True)
class User:
    id: int
    name: str
    email: str
    # 等価判定から除外(IDだけで等価とみなす)
    _display: str = field(init=False, repr=False, compare=False)

    def __post_init__(self):
        self._display = f"{self.name} <{self.email}>"

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.id == other.id

4.3 受け取りだけする一時値(InitVar

from dataclasses import dataclass, field, InitVar

@dataclass
class Record:
    raw: str
    strip: InitVar[bool] = True
    normalized: str = field(init=False)

    def __post_init__(self, strip):
        self.normalized = self.raw.strip() if strip else self.raw

5. よくある落とし穴とチェックリスト

5.1 ありがちミスの比較表

症状 原因 対策
TypeError: unhashable type eq=True かつ frozen=False キーにするなら frozen=Trueunsafe_hash=True(推奨は前者)
集合キーに入れた後で値を更新して壊れる 可変なのにハッシュ化 ハッシュ化しない/不変にする/キーに使わない
リスト/辞書の既定値が共有される default=[]default={} を書いた default_factory=list / dict に置換
異型比較で期待外れ 型違いでも False を返した NotImplemented を返す作法に直す
order=True で変な順序 意味のない順序比較を自動生成 ドメインの順序があるか再検討。必要なら自前で実装
slots=True で動的代入に失敗 __slots__ が属性追加を禁止 設計として動的属性をなくすか、slots を外す

5.2 ミュータブル既定値の正しい書き方

from dataclasses import dataclass, field

@dataclass
class Bag:
    items: list[str] = field(default_factory=list)  # これが正解

5.3 変更不能と後処理の相性

  • frozen=True でも __post_init__ 内だけは object.__setattr__ で派生値を設定可能。
  • 逆に、コンストラクト後に外部からフィールドを書き換える必要があるなら frozen=False を選ぶ。

6. まとめと最小テンプレ

「まず dataclass で“事実”を宣言し、__post_init__ で“整える”。“等価の意味”は必要なときだけ __eq__ で定義する。」これが実務で壊れにくい方針です。

6.1 最小テンプレ(値オブジェクト)

from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Value:
    a: int
    b: int
    _derived: int = field(init=False, repr=False, compare=False)

    def __post_init__(self):
        if self.a < 0 or self.b < 0:
            raise ValueError("a, b >= 0")
        object.__setattr__(self, "_derived", self.a + self.b)

6.2 最小テンプレ(近似等価)

import math
from dataclasses import dataclass

@dataclass(frozen=True)
class FloatBox:
    v: float
    def __eq__(self, other):
        if not isinstance(other, FloatBox):
            return NotImplemented
        return math.isclose(self.v, other.v, rel_tol=1e-9)

6.3 判断基準の一言まとめ(表)

判断ポイント こう書く なぜ
「初期化を安全に、最小コードで」 @dataclass + field 自動生成でバグ面積を減らす
「等価は意味で決めたい」 __eq__ を自前実装 近似/ID比較/異型対応を表現
「初期化後に整える」 __post_init__ 正規化/検証/派生値を一括
「ハッシュに使いたい」 frozen=True 不変+整合した __hash__ を自動

実務では「自前で書く前に dataclass で表現できないか」を常に検討し、__post_init__ に“整える責務”を寄せ、__eq__ ではドメインの等価を明示しましょう。これだけでクラス設計はかなり頑健になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?