コース進捗 コース目次 24レッスン中 24件を公開中
Pythonの基礎
データとコレクション
信頼できるプログラムを作る
オブジェクトでモデル化する
プロフェッショナルなPython
上級Python
データクラスは印刷済みの記録カード
普通のクラスでは、フィールドを受け取る処理、表示、比較だけでも何行も必要です。@dataclassは、その定型的な記録カードの仕組みを自動で作ります。
- 欄に名前 titleとminutes
- dataclass 定型メソッドを生成
- 記録 読みやすく比較できる
from dataclasses import dataclass
@dataclass
class Reading:
title: str
minutes: int
first = Reading("列挙型", 25)
second = Reading("列挙型", 25)
print(first)
print(first == second)
print(first is second)
Reading(title='列挙型', minutes=25)
True
False
既定では、__init__()、便利な__repr__()、同じクラスのフィールドを比べる__eq__()が生成されます。値が等しくても別オブジェクトなので、isはFalseです。フィールド注釈も実行時の引数を自動検証しません。この役割分担は第16章で扱います。
各カードに専用のポケットを付ける
生成される初期化メソッドはフィールド順に従うため、デフォルトなしのフィールドを、デフォルト付きフィールドより先に書きます。
@dataclass
class Article:
title: str
minutes: int
completed: bool = False
completedの後へ必須フィールドを置くと、クラス作成時にTypeErrorです。Falseのような単純な不変値は直接のデフォルトにできます。変更可能なリスト、辞書、集合にはファクトリーを使います。
- Akiのカード タグ用ポケットが必要
- Minaのカード 別のポケットが必要
- ファクトリー 毎回新しいリストを作る
- 共有しない タグが混ざらない
default_factoryは、変更可能な状態の意図しない共有を防ぎます。from dataclasses import dataclass, field
@dataclass
class Notebook:
owner: str
tags: list[str] = field(default_factory=list)
mine = Notebook("Aki")
yours = Notebook("Mina")
mine.tags.append("python")
print(mine.tags, yours.tags)
print(mine.tags is yours.tags)
['python'] []
False
渡すのはlist()の結果ではなく、括弧なしのファクトリーlistです。生成された初期化メソッドが、インスタンスごとに呼び出します。
欄がそろった後でカードを検査する
生成された初期化メソッドは、各フィールドを代入した後に__post_init__()を呼びます。小さな検証、正規化、派生フィールドの作成に使います。
from dataclasses import dataclass, field
@dataclass
class Session:
topic: str
minutes: int
label: str = field(init=False)
def __post_init__(self):
self.topic = self.topic.strip()
if not self.topic or self.minutes <= 0:
raise ValueError("topic and minutes must be valid")
self.label = f"{self.topic}({self.minutes}分)"
print(Session(" Python ", 30).label)
Python(30分)
init=Falseならlabelはコンストラクター引数になりません。ファイルアクセスや大きな処理は入れず、生成を予測しやすく保ちます。
@dataclass(frozen=True)は通常のフィールド再代入を防ぎます。変更版が必要ならdataclasses.replace()で新しい値を作ります。ただし、frozenの保証は浅いものです。
@dataclass(frozen=True)
class FrozenPocket:
tags: list[str] = field(default_factory=list)
pocket = FrozenPocket()
pocket.tags.append("まだ変わる")
print(pocket.tags)
['まだ変わる']
等価比較を生成したfrozenデータクラスには通常ハッシュも付きますが、比較対象フィールドにハッシュ不可能なリストがあれば、ハッシュ時に失敗します。深く安定させるなら文字列、数値、タプル、frozensetなどを選びます。unsafe_hash=Trueは、変わるデータを安全にする魔法ではありません。
Enumは選択肢が決まったメニュー
**列挙型(Enum)**は、自由な文字列を既知のメンバーオブジェクトへ置き換えます。
- メニュー LOW、NORMAL、HIGH
-
メンバー
Priority.HIGH -
保存値
"high"
from enum import Enum
class Priority(Enum):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
choice = Priority("high")
print(choice)
print(choice.name)
print(choice.value)
print(choice is Priority.HIGH)
print(choice == "high")
Priority.HIGH
HIGH
high
True
False
メンバーはEnum内のシングルトンなので、isによる同一性比較が一般的です。Priority("high")は値、Priority["HIGH"]は名前で検索します。未知の入力はValueErrorまたはKeyErrorです。通常のEnumは、値が比較できてもメンバー同士を<で並べられません。明示したキーや順位を使います。
ミニ制作:優先度付き読書キュー
reading_queue.pyを作ります。標準ライブラリだけで動く完成プログラムです。frozen項目データクラス、Enum、検証、default_factoryで専用リストを持つキューを組み合わせます。
from dataclasses import dataclass, field
from enum import Enum
class Priority(Enum):
LOW = "low"
NORMAL = "normal"
HIGH = "high"
RANK = {Priority.LOW: 1, Priority.NORMAL: 2, Priority.HIGH: 3}
@dataclass(frozen=True)
class ReadingItem:
title: str
minutes: int
priority: Priority = Priority.NORMAL
def __post_init__(self):
clean = self.title.strip()
if not clean:
raise ValueError("title must not be blank")
if (
isinstance(self.minutes, bool)
or not isinstance(self.minutes, int)
or self.minutes <= 0
):
raise ValueError("minutes must be a positive integer")
if not isinstance(self.priority, Priority):
raise TypeError("priority must be a Priority")
object.__setattr__(self, "title", clean)
@dataclass
class ReadingQueue:
name: str
minute_limit: int = 45
items: list[ReadingItem] = field(default_factory=list)
def add(self, item):
self.items.append(item)
def plan(self):
ordered = sorted(
self.items,
key=lambda item: (-RANK[item.priority], item.minutes, item.title),
)
chosen = []
used = 0
for item in ordered:
if used + item.minutes <= self.minute_limit:
chosen.append(item)
used += item.minutes
return chosen
queue = ReadingQueue("今夜")
for title, minutes, raw_priority in [
(" データクラス ", 25, "high"),
("Enumの境界", 15, "normal"),
("ハッシュのメモ", 10, "high"),
]:
queue.add(ReadingItem(title, minutes, Priority(raw_priority)))
plan = queue.plan()
print(f"{queue.name}:")
for number, item in enumerate(plan, start=1):
print(f"{number}. [{item.priority.name}] {item.title} - {item.minutes}分")
print(f"合計:{sum(item.minutes for item in plan)}分")
print(f"一意な項目:{len(set(plan))}")
print(f"新しいキューは空:{ReadingQueue('明日').items == []}")
python3 reading_queue.pyで実行し、次を確認します。
今夜:
1. [HIGH] ハッシュのメモ - 10分
2. [HIGH] データクラス - 25分
合計:35分
一意な項目:2
新しいキューは空:True
frozen項目は、比較される全フィールドがハッシュ可能なので集合へ入れられます。境界の注意:注釈だけではPriorityを強制しないため、__post_init__()で確認しています。また、負の制限時間はこの小さなキューの契約外なので、利用者入力を受ける前に検証が必要です。
3つの小さなミッション
- ファクトリーを証明。 2つのキューを作り、片方だけに追加して、もう片方のリストが空だと確かめます。
- frozenのコピー。
dataclasses.replace()で、元を変えずに完了済みの項目を作ります。 - Enumの境界。 正しい優先度値と未知の値を1つずつ解析し、未知の選択肢へ明確なエラーを出します。
第16章へ進む準備
- 既定の
@dataclassが何を生成し、isが何を意味するか分かる - 必須フィールドをデフォルト付きフィールドより前に置ける
- 独立した変更可能デフォルトに
default_factoryを使える __post_init__()で小さな検証や派生値を作れる- frozenが浅く、ハッシュ可能性は全フィールド次第だと説明できる
- Enumメンバー、その
name、valueを区別できる - メンバーを同一性で比較し、外部文字列を境界で解析できる
- キューを実行し、「新しいキューは空:True」を確認できる
次章では、静的ツールにもデータと振る舞いの契約が見えるよう、型ヒントとプロトコルを加えます。