Python講座 / dataclassでデータを表す

dataclassは型注釈付きのフィールドを基に、初期化、表示、等価比較などを生成する仕組みです。型注釈だけで実行時の入力検証が自動化されるわけではないため、必要な規則は明示します。

データ表現と検証を区別する

支出の名前、金額、タグをまとめます。__init__や__repr__、__eq__を毎回手書きする負担を減らせますが、金額が非負の整数かは__post_init__で検査します。

tagsのようなリストにはfield(default_factory=list)を使い、インスタンスごとに新しいリストを作ります。共有する一つのリストを既定値にする設計とは異なります。

フィールドの共有とJSON変換は、インスタンス属性とクラス属性で扱う規則とつながります。

独立したタグ一覧を実行で確認する

data_records.pyへ保存し、python data_records.pyを実行します。

from dataclasses import dataclass, field

@dataclass
class Expense:
    name: str
    amount: int
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if type(self.amount) is not int or self.amount < 0:
            raise ValueError("invalid amount")

a = Expense("lunch", 8000)
b = Expense("bus", 2500)
a.tags.append("work")
print(a)
print(b.tags)
print(a == Expense("lunch", 8000, ["work"]))

予想結果は次のとおりです。aのタグを変更してもbのタグは空のままです。

Expense(name='lunch', amount=8000, tags=['work'])
[]
True

最後のTrueは、比較対象のフィールド値が同じであることを示します。

初期化後の変更と不変性を考える

__post_init__の検査は初期化時のもので、後からa.amount = -1と代入する操作を止めません。更新時も規則を守る必要があるなら、プロパティなどを使うか、変更ではなく新しい値を作る設計を検討します。

frozen=Trueは通常のフィールド再代入を制限しますが、中のリストへappendすることまで防ぐ深い不変性ではありません。内部も変更させたくないなら、タプルなどの型と公開する操作を含めて設計します。

等価性と保存形式の境界を確認する

同じ名前・金額・タグを持つ二つの支出が等しいことと、同じ取引であることは別です。二度の同額の昼食を一つへまとめないためには、取引IDなどの識別規則が必要です。

タグの独立性、負の初期値の拒否、同じフィールド値の比較をそれぞれ試してください。asdictで辞書にできても、日付やDecimalなどが自動でJSON化できるとは限りません。JSONへの変換では保存先で許される型も確認します。

dataclassはフィールド宣言から__init__・__repr__・__eq__などを生成しますが、業務ルールを自動検証するわけではありません。初期化や表示の定型処理を減らします。個別のリストにはfield(default_factory=list)を使い、frozen=Trueでも内部リストまでは不変にならないことを区別します。

関連する公式リファレンス