この記事のポイント

  • 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)
@Reviewer
API契約で可変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"}
@Reviewer
set型はハッシュ不可です。キー用途では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レビューは現場設計育成で極めて効果的です。