frozen=True でイミュータブルなデータクラスを作る|Python
Python の dataclass はデフォルトでミュータブルだ。インスタンス生成後にフィールドを自由に書き換えられる。しかし、設定値や座標のように「一度作ったら変更されるべきでない」データも多い。そうしたケースで使うのが frozen=True オプションである。
frozen=True の基本
@dataclass(frozen=True) を指定すると、インスタンスのフィールドへの代入が禁止される。値を変更しようとすると FrozenInstanceError が発生する。
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p.x) # 1.0
p.x = 3.0 # FrozenInstanceError内部的には __setattr__ と __delattr__ がオーバーライドされ、属性の変更・削除を一律にブロックする仕組みになっている。通常の dataclass が生成する __setattr__ をそのまま使うのではなく、例外を送出するバージョンに差し替えているわけだ。
通常の dataclass との比較
frozen を指定しない場合と比べると、振る舞いが明確に変わる。
フィールドへの再代入が自由にできる。辞書のキーや集合の要素には使えない。__hash__ はデフォルトで None になる。
フィールドへの再代入で FrozenInstanceError が発生する。__hash__ が自動生成されるため、辞書のキーや集合の要素として使える。
この違いは設計上の判断に直結する。データが変更されないことをコードレベルで保証できるのは、バグの予防において大きな意味を持つ。
ハッシュ可能になる利点
frozen なデータクラスは __hash__ が自動生成される。これにより、dict のキーや set の要素として使えるようになる。
from dataclasses import dataclass
@dataclass(frozen=True)
class Color:
r: int
g: int
b: int
palette = {
Color(255, 0, 0): "red",
Color(0, 128, 0): "green",
Color(0, 0, 255): "blue",
}
print(palette[Color(255, 0, 0)]) # red
unique_colors = {Color(255, 0, 0), Color(0, 0, 255), Color(255, 0, 0)}
print(len(unique_colors)) # 2通常の dataclass でこれをやると TypeError: unhashable type になる。ハッシュ可能であることは、キャッシュやルックアップテーブルを構築するときにも重要だ。
値を変更したい場合は replace を使う
frozen なインスタンスは変更できないが、一部のフィールドだけ変えた新しいインスタンスを作ることはできる。dataclasses.replace を使えばよい。
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool
base = Config(host="localhost", port=8080, debug=False)
dev = replace(base, debug=True)
print(base) # Config(host='localhost', port=8080, debug=False)
print(dev) # Config(host='localhost', port=8080, debug=True)
print(base is dev) # False元のインスタンスはそのまま残り、変更箇所だけ反映された別のオブジェクトが生成される。関数型プログラミングでいう「永続データ構造」に近い考え方で、状態の変遷を追跡しやすくなる。
post_init での初期化
frozen なデータクラスでは __post_init__ 内であっても通常の代入はできない。object.__setattr__ を使って直接書き込む必要がある。
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Circle:
radius: float
area: float = field(init=False)
def __post_init__(self):
object.__setattr__(self, "area", 3.14159 * self.radius ** 2)
c = Circle(radius=5.0)
print(c.area) # 78.53975self.area = ... と書くと FrozenInstanceError になるため、object.__setattr__ で Python のオブジェクトシステムを直接叩く。これは frozen dataclass における定番のパターンである。
この object.<span class="rollpie-article-note__mark">setattr</span> による書き込みは <span class="rollpie-article-note__mark">post_init</span> 内でのみ使うべきで、外部から呼び出すのは frozen の意図に反する。あくまで初期化時の計算フィールド設定に限定するのが望ましい。
生成後に外部から object.setattr を使えば変更は可能だが、それは設計上の契約違反になる。
frozen と継承
frozen な dataclass を継承する場合、子クラスも frozen でなければならない。frozen と非 frozen の混在は許可されていない。
from dataclasses import dataclass
@dataclass(frozen=True)
class Base:
x: int
@dataclass(frozen=True)
class Child(Base):
y: int
c = Child(x=1, y=2)
print(c) # Child(x=1, y=2)もし子クラスで frozen=True を外すと TypeError が発生する。
from dataclasses import dataclass
@dataclass(frozen=True)
class Base:
x: int
@dataclass # frozen=True がない
class Child(Base):
y: int
# TypeError: cannot inherit frozen dataclass from a non-frozen onefrozen=True の dataclass を継承するとき、子クラスはどうするべきか?
- frozen を指定しなくてよい
- 子クラスにも frozen=True を指定する
- eq=False を指定する
- __hash__ を手動で定義する
frozen を使うべき場面
frozen dataclass が特に効果を発揮するのは、次のような場面だ。
アプリケーション設定、接続情報、環境変数をまとめたオブジェクトなど。一度読み込んだら変更されるべきでないデータに適している。
ハッシュ可能であることが求められる場面。複合キー(複数のフィールドを組み合わせた一意識別子)としても使える。
イミュータブルなオブジェクトはスレッド間で安全に共有できる。ロックなしで読み取りが可能になるため、並行処理のコードが簡潔になる。
一方で、頻繁にフィールドを更新するようなデータには向かない。毎回 replace で新しいインスタンスを作るのはオーバーヘッドになるため、そうしたケースでは通常の dataclass を使うほうが合理的である。設計段階で「このデータは変更されるか」を判断し、適切に使い分けることが重要だ。












frozen な親クラスを継承する場合、子クラスにも frozen=True が必要です。frozen と非 frozen の混在は TypeError になります。