Python|dataclass(frozen)によるイミュータブル設計の考え方
この記事のポイント
- dataclass(frozen=True)の活用ポイントをレビューできる
- イミュータブル設計によるAPI契約の安全性向上を理解できる
- 変更責務の分離整理をレビュー技法として学べる
そもそもdataclass(frozen)とは
Pythonのdataclassは柔軟なデータ構造を提供します。
さらにfrozen=Trueを指定することでイミュータブル(不変)なデータ構造が定義できます。
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int特徴は以下です。
- 初期化後の属性変更禁止
__hash__自動実装 → set/dictのキーに利用可能- API契約の変更不可能性を型で保証
- スレッドセーフ設計に有効
なぜこれをレビューするのか
現場で頻発する失敗は以下です。
- API契約に可変dataclassを使用
- 内部状態保持も無条件でfrozen適用
- 更新責務の整理不足
- ハッシュ利用可否設計漏れ
- 不変性必要箇所の見極めミス
レビューアーは「このオブジェクトは変更される設計責務か?」を常に読み取ります。
レビューアー視点
- frozen指定が責務整理に一致しているか
- API契約と実装責務の不変性整合が取れているか
- 内部状態保持で誤ってfrozen適用されていないか
- ハッシュ利用可能性が考慮されているか
- 実行時変更漏洩リスクを事前に遮断できているか
開発者視点
- API入出力・DTO用途はfrozen優先
- 内部更新責務を持つモデルは非frozen
- ハッシュキー候補はfrozen適用
- 更新責務整理は設計段階で定義
- frozen適用範囲は最小必要限に限定
良い実装例
なぜこの実装が良いのか
- API層のDTOにfrozen適用
- 内部処理層では柔軟dataclass利用
- 更新責務を型に明確化
- ハッシュ可能性も維持可能
# api_dto.py
from dataclasses import dataclass
@dataclass(frozen=True)
class ApiRequest:
request_id: int
endpoint: str
client_ip: str# domain_model.py
@dataclass
class ApiRequestLog:
request_id: int
endpoint: str
client_ip: str
archived: bool = False補足
API契約は不変性が型に反映、内部状態は更新責務に応じ柔軟化されています。レビューアーはこの責務線引きを重点確認します。
レビュー観点
- API契約で不変型が採用されているか
- 内部状態でfrozen誤用が無いか
- 更新責務が型定義に反映されているか
- ハッシュ利用可能性設計が考慮されているか
- frozen適用範囲が適切に限定されているか
良くない実装例: ケース1(API契約で可変dataclass)
# bad_mutable_api_dto.py
from dataclasses import dataclass
@dataclass
class ApiRequest:
request_id: int
endpoint: str
client_ip: str
@ReviewerAPI契約DTOにmutable dataclassを使用しています。frozen=Trueを検討してください。
問題点
- 呼び出し側が意図せず値変更可能
- バグ混入経路
- API契約破壊リスク
改善例
# good_api_frozen_dto.py
from dataclasses import dataclass
@dataclass(frozen=True)
class ApiRequest:
request_id: int
endpoint: str
client_ip: strAPI契約は不変型優先が原則。レビューではAPI層型定義で即確認します。
良くない実装例: ケース2(内部状態管理でfrozen誤用)
# bad_internal_frozen.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Session:
user_id: str
attributes: dict[str, str]
@Reviewer内部状態保持にfrozenを指定し可変属性を内包しています。整合性崩壊リスクがあります。
問題点
- dict型属性はmutable
- 外部経由で実質更新可能
- frozen指定の意図消失
改善例
# good_internal_non_frozen.py
from dataclasses import dataclass
@dataclass
class Session:
user_id: str
attributes: dict[str, str]内部可変構造は非frozenで自然表現が基本原則。レビューでは属性型まで確認します。
良くない実装例: ケース3(ハッシュ利用検討漏れ)
# bad_missing_hashability.py
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
points = {Point(1, 2)}
@Reviewerhash可能性が不定です。ハッシュ利用予定があるならfrozen指定を検討してください。
問題点
- hashability未定義
- 実行時TypeError可能性
- 設計意図不明化
改善例
# good_hashable_point.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
points = {Point(1, 2)}集合/辞書キー候補はhashable保証が原則。レビューではハッシュ利用想定有無を確認します。
良くない実装例: ケース4(frozen範囲の不必要拡大)
# bad_overfrozen.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
timeout: int
retries: int
logger: object
@Reviewerlogger等の依存注入構造までfrozen化しています。必要最小限の凍結を検討してください。
問題点
- 非データ属性まで凍結
- DI柔軟性低下
- 保守性悪化
改善例
# good_responsibility_split.py
from dataclasses import dataclass
@dataclass(frozen=True)
class RetryPolicy:
timeout: int
retries: int
@dataclass
class Config:
policy: RetryPolicy
logger: objectデータ責務だけをfrozen化し依存性責務は柔軟化が安定設計。レビューでは責務ごとに凍結範囲を評価します。
観点チェックリスト
まとめ
dataclass(frozen)レビューは「変更責務と契約安全性を型で制御する訓練」です。
レビューアーは常に
- どこが誰の責務で変更されるか?
- API契約は守られる型構造か?
- 不変保証が過剰/不足になっていないか?
を読み取り、安全な設計意図を型構造で自然に読み取れる状態をレビュー実践の中で作っていきます。
frozenレビューは現場設計育成に非常に有効です。