CHAPTER 4 ・ コードを整理して育てる / Lesson 16
dataclass と型ヒント
データを入れる箱を、数行で定義する。型ヒントを添えて、何が入るのかをコードに書き残します。
このレッスンが終わったら
「dataclass で、データ用のクラスを短く作れる。型ヒントは実行を変えないが、読む人とAIへの説明になる」と言える。
こんな場面、ありませんか?
売上明細の1行(伝票番号・日付・数量・単価)を、Lesson 15 のクラスで「1つの物」として持ってみます。すると、データを入れるだけのクラスでも、かなり長く書くことになります。
▶ 実行を押すと、ここに結果が出ます
同じ名前(slip_no など)を、__init__ の引数、self.○○ = ○○、__repr__ と、3回ずつ書いています。項目が増えると、書く量も、書き間違いも増えます。
この「データを入れる箱」を、数行で作れるのが、この回の主役、dataclass(データクラス)です。もう1つの主役、型ヒント(何が入る予定かの書き添え)も、あわせて学びます。
dataclass で書いてみる
同じ4つの項目を、dataclass で書きます。
▶ 実行を押すと、ここに結果が出ます
__init__ も __repr__ も書いていないのに、作れて、print で中身が読める形で表示されました。しかも、項目名は1回ずつ書くだけです。
仕組みを読む
slip_no項目の名前(属性の名前):「これは〜の予定」の印str入れる値の型(文字)@dataclassこの下のクラスを、「データ入れ」として作る。__init__ と __repr__ を自動で作ってくれるclass SalesLine:SalesLine(売上明細の1行)という設計図を作る slip_no: str「伝票番号」という欄を作る。文字(str)が入る予定 qty: int「数量」という欄を作る。整数(int)が入る予定日本語にすると
@dataclass のように、クラスや関数の上に @ で付ける書き方をデコレータ(飾り)と呼びます。「このクラスに、便利な機能を足す」という印だと思えば十分です。
: str や : int が、型ヒントです。よく使う型は、次の通りです。
| 書き方 | 意味 |
|---|---|
int / str / float / bool | 整数 / 文字 / 小数 / 真偽 |
list[str] | 文字のリスト |
dict[str, int] | キーが文字、値が整数の辞書 |
str | None | 文字、または None(値がない) |
変えて、試す
① 型ヒントは、守られない。 型ヒントは「こういう値が入る予定」という書き添えで、Pythonは守らせません。CSVから読んだ数字は文字なので、ためしに文字のまま入れてみます。
▶ 実行を押すと、ここに結果が出ます
作る時点ではエラーにならず、print もできました(qty='5' と、引用符付きで文字だと分かります)。ところが、掛け算で TypeError になりました。型ヒントは、エラーを防いでくれるのではなく、読む人(とAI)への説明です。実際に変換するのは、次の __post_init__ の仕事です。
② 作った直後に、変換と点検をする。 __post_init__ は、dataclass が値を受け取った直後に自動で動くメソッドです。ここで int() に変換し、おかしな値は ValueError で止めます。sales_detail.csv を、まるごと SalesLine にしてみます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
128件が読めて、9月の合計は Lesson 13 で求めた 1036500 と一致しました。month_total(lines: list[SalesLine], month: str) -> int のように、関数の引数と戻り値にも型ヒントを付けられます(-> のあとが、戻り値の型です)。実行には影響しませんが、「何を渡して、何が返るか」が、関数の1行目で読めます。
③ 書き換えを禁止する:frozen=True。 一度作ったデータを、うっかり書き換えたくないときは、@dataclass(frozen=True) にします。
▶ 実行を押すと、ここに結果が出ます
FrozenInstanceError(固定されたデータは書き換えられません)と出ます。請求や入金のデータなど、後から変わってはいけないものに向いています。
④ 初期値と、リストの初期値。 初期値は = 値 で付けます。リストなどを初期値にするときは、Lesson 13 の罠と同じ理由で、field(default_factory=list) と書きます。
▶ 実行を押すと、ここに結果が出ます
tags: list[str] = [] と書くと、ValueError: mutable default ... use default_factory と、Python が止めてくれます。a と b の tags は別々のリストになっています。
自分で書く
お題① 入金1件を表す Payment を、dataclass で定義してください。項目は、date(文字)・memo(文字)・amount(整数)です。3件のリストから、入金額の合計を表示します。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
@dataclass の下に class Payment: を書き、date: str / memo: str / amount: int の3行を並べます。
答えの例を見る
from dataclasses import dataclass
@dataclass
class Payment:
date: str
memo: str
amount: int
payments = [
Payment("2025-10-28", "INV-2025-0001", 1320000),
Payment("2025-10-29", "INV-2025-0002", 484000),
Payment("2025-10-30", "INV-2025-0004", 230000),
]
print(sum(p.amount for p in payments))お題② expenses.csv の各行を Expense にします。CSVの金額は文字なので、__post_init__ で int に直してください。そのうえで、総務部の金額の合計を表示します。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
def __post_init__(self): の中で、self.amount = int(self.amount) と書きます。字下げ(インデント)をそろえましょう。
答えの例を見る
import csv
from dataclasses import dataclass
@dataclass
class Expense:
no: str
dept: str
amount: int
def __post_init__(self):
self.amount = int(self.amount)
with open("expenses.csv", encoding="utf-8") as f:
expenses = [Expense(r["申請ID"], r["部署"], r["金額"]) for r in csv.DictReader(f)]
print(sum(e.amount for e in expenses if e.dept == "総務"))仕事ならどう使う?
理解チェック@dataclass class P: qty: int のクラスに、P("5") と文字を渡しました。どうなるでしょう?
理解チェックdataclass を使うと、自動で作られるものは、どれでしょう?