Python|frozensetを使った不変集合の適用判断と設計整理
この記事のポイント
- frozensetの適切な適用場面をレビューできる
- 不変性と集合責務の分離設計が身につく
- 集合の比較・格納・契約の安全設計をレビュー技法として理解できる
そもそもfrozensetとは
Python標準型frozensetは、イミュータブルな集合型です。
fs = frozenset([1, 2, 3])特徴は以下です。
- 集合演算は可能(和・積・差・部分集合判定など)
- 要素の変更・追加・削除は不可
- dictのキーやsetの要素に利用可能
- ハッシュ可能(set自体は非ハッシュ)
「変更できない安全な集合」を提供する型です。
なぜこれをレビューするのか
現場では以下のような失敗が頻発します。
- setをmutableのまま流用
- API契約に変更可能集合を渡す設計崩壊
- 不変性保証が型で担保されていない
- デフォルト引数でsetを利用して事故
- 集合状態の契約境界線が曖昧
レビューアーは「この集合は変更されるべき責務か?」を常に読み取ります。
レビューアー視点
- frozenset利用理由が設計責務に一致しているか
- 集合状態の契約境界が明示されているか
- API契約で不変性が型に反映されているか
- 集合の可変性による事故リスクが排除されているか
- 集合論的演算の責務が適切に整理されているか
開発者視点
- API入力はfrozenset優先
- 内部状態保持はsetで柔軟性確保
- 集合比較・メンバー判定にはfrozenset活用
- dictキー用途ではfrozenset活用必須
- デフォルト引数には絶対mutable集合を使わない
良い実装例
なぜこの実装が良いのか
- API契約としてfrozenset使用で不変性明示
- 内部処理ではset活用で柔軟性保持
- 集合状態の更新責務と契約責務が分離
- ハッシュ可能性も維持可能
# permission_api.py
from typing import FrozenSet
class PermissionService:
def has_access(self, user_roles: FrozenSet[str], required_roles: FrozenSet[str]) -> bool:
return not required_roles.isdisjoint(user_roles)補足
API契約にfrozensetを使うことで「変更されないこと」を型で保証しています。レビューアーは型定義と契約責務を一致させます。
レビュー観点
- API契約層でfrozenset利用が整理されているか
- 内部処理層で柔軟にset活用できているか
- 集合比較・格納時にfrozenset活用が徹底されているか
- 不変性保証が型定義に反映されているか
- デフォルト引数でmutable setを使用していないか
良くない実装例: ケース1(API契約でmutable set使用)
# bad_mutable_set_api.py
from typing import Set
class PermissionService:
def has_access(self, user_roles: Set[str], required_roles: Set[str]) -> bool:
return not required_roles.isdisjoint(user_roles)
@ReviewerAPI契約で可変Set型を使用しています。API層では不変frozenset型を採用してください。
問題点
- 呼び出し側が引数変更可能
- API契約責務が緩すぎる
- バグ混入余地
改善例
# good_frozenset_api.py
from typing import FrozenSet
class PermissionService:
def has_access(self, user_roles: FrozenSet[str], required_roles: FrozenSet[str]) -> bool:
return not required_roles.isdisjoint(user_roles)API契約は常に最小権限設計(read-only思想)で型定義します。レビューではAPI契約型を即座に確認します。
良くない実装例: ケース2(内部状態保持にfrozenset誤用)
# bad_internal_frozenset.py
class RoleManager:
def __init__(self):
self.roles = frozenset()
def add_role(self, role: str):
self.roles = self.roles.union([role])
@Reviewer内部状態保持にfrozensetを使用し毎回新インスタンス生成しています。内部は可変setで管理してください。
問題点
- 毎回新frozensetインスタンス生成コスト
- 可読性・効率性低下
- 柔軟性不足
改善例
# good_internal_set.py
class RoleManager:
def __init__(self):
self.roles: set[str] = set()
def add_role(self, role: str):
self.roles.add(role)内部状態はset、API契約はfrozensetという役割分離が原則です。レビューでは内部構造型を重点確認します。
良くない実装例: ケース3(dictキーでset使用)
# bad_set_as_dict_key.py
roles = set(["admin", "editor"])
user_map = {roles: "UserA"}
@Reviewerset型はハッシュ不可です。キー用途ではfrozensetへ変換してください。
問題点
- setはmutable→ハッシュ不可
- 実行時例外発生
- データ構造破綻
改善例
# good_frozenset_as_key.py
roles = frozenset(["admin", "editor"])
user_map = {roles: "UserA"}dictキー用途ではfrozenset必須。レビューではハッシュ利用有無を確認します。
良くない実装例: ケース4(デフォルト引数にmutable set使用)
# bad_mutable_default_argument.py
def register_roles(roles=set()):
roles.add("default")
@Reviewerデフォルト引数にmutable型を使用しています。初期値生成関数化で防止してください。
問題点
- デフォルト引数共有事故
- 状態汚染
- バグ混入温床
改善例
# good_safe_default.py
def register_roles(roles=None):
if roles is None:
roles = set()
roles.add("default")mutable型のデフォルト引数禁止はレビュー初期確認項目です。frozenset議論時も必ず併せて確認します。
観点チェックリスト
まとめ
frozenset設計レビューは「集合の変更責務を型で表現できているか」の可視化訓練です。
レビューアーは常に
- この集合は誰が変更可能なのか?
- API契約型に不変性が反映されているか?
- ハッシュ利用可否を型で安全化しているか?
を読み取り、「変更禁止の設計意図をコードから自然に読み取れる状態」を作るレビュー習慣を身につけていきます。
frozensetレビューは現場設計育成で極めて効果的です。