まえがき
今回はデータクラスについて扱う。
本来のクラスは、データ(インスタンス変数)と関連する機能(メソッド)の集合体。しかし、実際のアプリではデータ(の集合)だけを扱うクラスが一定数存在する。
→ データクラス:Python3.7以降ではそのようなクラスを表現するための仕組みが用意されている。
これにより以下のようなメリットを享受できる
ⅰ) 基本メソッドを自動生成してくれる
以下のメソッドは自動生成してくれる。
__init__, __repr__, __eq__
__lt__, __le__, __gt__, __ge__, __hash__
ただし、2列目のメソッドは、指定のオプションが指定された場合にのみ生成される。
ⅱ) イミュータブルなオブジェクトを生成する
標準的なclass命令で生成されるオブジェクトは、既定でミュータブル。通常のクラスであれば、これをイミュータブルにするとなるとやや複雑だが、データクラスであれば frozen オプションを有効になるので、イミュータブルなクラスを定義可能。
ⅲ) メソッドは自由に追加できる
自動生成されるメソッドばかりではない。これまでのclass命令と同様に。def命令で明示的に独自のメソッドを追加可能。__init__ などの予約メソッドも、明示的に宣言された場合にはそちらが優先される。
上記のようなメソッドからも、利用が許される場合には、積極的にデータクラスを用いるべき。
データクラスの基本
具体例を示す。firstname/lastname/age属性、showメソッドを備えた Person クラスをデータクラスとして実装することを考える。
import dataclasses
@dataclasses.dataclass(frozen = True)
class Person:
firstname: str
lastname: str
age: int = 0
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
if __name__ == '__main__':
p1 = Person('太郎', '山田', 58)
p2 = Person('太郎', '山田', 58)
print(p1) # Person(firstname='太郎', lastname='山田', age=58)
print(p1 == p2) # True
データクラスを用いる点でのポイントは以下。
・@dataclassデコレーターを付与
データクラスを宣言するには、@dataclasses.dataclassデコレ―タを付与するのみ。主要なメソッドは以下の通り。
| オプション名 | デフォルト | 説明 |
|---|---|---|
| init | True |
__init__ メソッドを自動生成するかどうか。False にすると生成されない。 |
| repr | True |
__repr__ メソッドを自動生成するかどうか。デバッグ用の文字列表現を持つ。 |
| eq | True |
__eq__ メソッドを自動生成するかどうか。インスタンス同士の比較が可能になる。 |
| order | False |
<, <=, >, >= の比較メソッドを自動生成するかどうか。eq=True のときのみ有効。 |
| unsafe_hash | False |
__hash__ を強制的に自動生成するかどうか。通常はイミュータブル(frozen=True)でないと自動生成されない。 |
| frozen | False | インスタンスをイミュータブル(変更不可)にするかどうか。True にするとフィールドの代入を禁止する。 |
| slots (3.10+) | False |
__slots__ を自動生成するかどうか。インスタンスのメモリ効率を改善できる。 |
| kw_only (3.10+) | False | すべてのフィールドをキーワード専用引数にするかどうか。 |
| match_args (3.10+) | True |
__init__ 時のパラメーターに基づいて __match_args__ を生成するかどうか。 |
クラスはミュータブルである方が扱いが簡単である点に注意(オブジェクトの状態が途中で意図せずに帰られてしまう心配がないから)。要件を満たす限り、クラスはイミュータブルとするのが理想。
・フィールドを宣言する
フィールド:データクラスが保持するインスタンス変数
フィールド名: 型 [= 既定値]
の形で記述する。
firstname: str
lastname: str
age: int = 0
は内部的には以下のような __init__ メソッドが生成されている。(引数はフィールドの宣言順序に従うことに注意)
・メソッドを宣言する
冒頭でもふれたように、データクラスにも任意のメソッドを追加できる。これはこれまでと同じ構文なので、特筆すべきこともない。
・データクラスの初期化と比較
定義されたデータクラスは、__if __name__ == '__main__':
のように初期化でき、「==」演算子でも比較可能。これは既定ですべてのフィールドが比較対象。
フィールドのカスタマイズ
dataclass.field 関数:フィールドの挙動は様々にカスタマイズ可能
以下は具体例。ageフィールドを __eq__ メソッドの判定から除外する例。
import dataclasses
@dataclasses.dataclass()
class Person:
firstname: str
lastname: str
age: int = dataclasses.field(default=0, compare=Flase)
if __name__ == '__main__'
p1 = Person('太郎', '山田', 58)
p2 = Person('太郎', '山田', 11)
print(p1 == p2) # True
フィールド情報は、field関数のキーワード引数として宣言する。
| 引数名 | デフォルト | 説明 |
|---|---|---|
| default | フィールドのデフォルト値を指定する。default_factory とは同時に使えない。 |
|
| default_factory | デフォルト値を返す関数を指定する。list, dict などミュータブルなデフォルト値を使う場合に有効。 |
|
| init | True |
__init__ の引数として含めるかどうか。 |
| repr | True |
__repr__ の出力に含めるかどうか。 |
| compare | True | 比較演算 (==, < など) に使うかどうか。 |
| hash | None |
__hash__ 計算に含めるかどうか。None の場合、compare の値に従う。 |
| metadata | None | ユーザー定義の任意メタデータを辞書形式で持たせることができる。 |
| kw_only (3.10+) | False |
True にすると、そのフィールドはキーワード専用引数になる。 |
例えば、初期化時に指定したくない指定したくないフィールドでは init オプションを False にするし、同値/大小判定に関与しないフィールドはcompareオプションを False にして除外。
field関数に諸々の情報を指定すれば、あとはフィールドの初期値として渡してしまえばよい。field関数が初期値を占めてしまうので、初期値を持つ field 関数を利用する場合には、default オプションを利用。
イミュータブルなクラス
データクラスではfrozenオプションの付与によって、簡単にイミュータブルなクラスの宣言が可能。
例えば、既に出した例で考えてみる。p1.firstname = '次郎' と変更したいる。
import dataclasses
@dataclasses.dataclass(frozen=True)
class Person:
firstname: str
lastname: str
age: int = 0
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
if __name__ == '__main__':
p1 = Person('太郎', '山田', 58)
p2 = Person('太郎', '山田', 58)
print(p1 == p2) # True(eq=True がデフォルト)
p1.firstname = '次郎' # ← ここで FrozenInstanceError
ただし、frozenオプションを付与しても、不変性が敗れる場合がある(というか、簡単に破れる)
例えば、先ほどの Person クラスに list 型の memo フィールドを追加する。
import dataclasses
@dataclasses.dataclass(frozen=True)
class Person:
firstname: str
lastname: str
age: int = 0
memos: list[str] = dataclasses.field(default_factory=list)
# 1) 外のリストを共有すると中身は後から変わる
ms = ['married', 'AB']
p = Person('太郎', '山田', 58, ms)
ms.append('dog') # ← p.memos にも反映される
print(p) # Person(..., memos=['married', 'AB', 'dog'])
# 2) キーワード名と属性名を正す
p = Person('太郎', '山田', 58, memos=['married', 'AB'])
p.memos.append('dog') # ← これは通る(再代入でなくミューテーション)
print(p) # Person(..., memos=['married', 'AB', 'dog'])
ミュータブルの既定値を渡す場合、dataclasses.field関数で既定値を生成するための関数を渡す。( memos: list = [] のような記述はエラー)
Pythonにおける値はオブジェクト渡しが基本。よって、引数/戻り値で受け渡した値を変更した場合、その内容はフィールドにも影響する。これを防ぐためにはイミュータブルである tuple 型を用いるのがよい。
# 不変コンテナを使って管理
from dataclasses import dataclass
from typing import Tuple
@dataclass(frozen=True)
class Person:
firstname: str
lastname: str
age: int = 0
memos: Tuple[str, ...] = () # ← タプルにする
# 1) 外のリストを共有すると中身は後から変わる
ms = ['married', 'AB']
p = Person('太郎', '山田', 58, ms)
ms.append('dog') # ← p.memos にも反映される
print(p) # Person(..., memos=['married', 'AB', 'dog'])
# 2) キーワード名と属性名を正す
p = Person('太郎', '山田', 58, memos=['married', 'AB'])
p.memos.append('dog') # ← これは通る(再代入でなくミューテーション)
print(p) # Person(..., memos=['married', 'AB', 'dog'])
import dataclasses
from typing import Iterable, Tuple
@dataclasses.dataclass(frozen=True)
class Person:
firstname: str
lastname: str
age: int = 0
memos: Tuple[str, ...] = dataclasses.field(default_factory=tuple)
def __post_init__(self):
# list などで渡されてもタプルに固める(frozenでも object.__setattr__ は可能)
object.__setattr__(self, 'memos', tuple(self.memos))
if __name__ == '__main__':
# 外部のリストを渡す
ms = ['married', 'AB']
p1 = Person('太郎', '山田', 58, ms)
ms.append('dog') # 外部リストを変更
print(p1) # Person(firstname='太郎', lastname='山田', age=58, memos=('married', 'AB'))
# キーワード引数で直接渡す
p2 = Person('花子', '佐藤', 30, memos=['single', 'O'])
print(p2) # Person(firstname='花子', lastname='佐藤', age=30, memos=('single', 'O'))
# p2.memos.append('X') # AttributeError: 'tuple' object has no attribute 'append'
hashableなクラスを生成
@dataclass は eq / frozen / unsafe_hash の値により __eq__ と __hash__ を自動生成/抑制。要点は次表。
| 設定 | 生成される __eq__
|
生成される __hash__
|
結果 |
|---|---|---|---|
eq=True(既定), frozen=False(既定) |
生成される | None に設定 |
非ハッシュ可能(TypeError: unhashable type) |
eq=True, frozen=True
|
生成される | 自動生成 | 値オブジェクトとしてハッシュ可能 |
eq=False |
生成されない | 既定の object.__hash__ が残る |
同一性ベースでハッシュ可能(isで比較) |
unsafe_hash=True(eq=True かつ frozen=False など) |
生成される | 強制生成 | ハッシュ可能だが自己責任(不変性担保はあなた次第) |
・「eq=True かつ frozen=False」は危険(可変なのに等値比較あり)なので、データクラスはあえてハッシュ不能にする( __hash__=None )。
・凍結(frozen=True)なら値が変わらない前提なので __hash__ を自動生成してくれる。
・eq=Falseなら「値の equality を定義しない」= 同一性でのハッシュになる。
・どうしても可変と等値比較を両立しつつハッシュにしたいならunsafe_hash=True(推奨はしない)。
データクラスの予約関数
データクラスには、クラスを取得/操作するために、以下のような関数を用意している。
fields():すべてのフィールドを取得する。
・役割:データクラスに定義されたフィールド情報を取得する。
・戻り値:Field オブジェクトのタプル(各フィールド名・型・既定値などを含む)。
from dataclasses import dataclass, fields
@dataclass
class Person:
firstname: str
lastname: str
age: int = 0
for f in fields(Person):
print(f.name, f.type, f.default)
firstname <class 'str'> <dataclasses._MISSING_TYPE object>
lastname <class 'str'> <dataclasses._MISSING_TYPE object>
age <class 'int'> 0
→ introspection(内部調査)や自動ドキュメント生成に役立つ。
asdict(), astuple():dict/tuple型に変換する
・役割:データクラスのインスタンスを再帰的に辞書/タプル化する。
・特徴: 内部に別のデータクラスがあっても自動的に辞書/タプル化してくれる。
from dataclasses import dataclass, asdict
@dataclass
class Address:
city: str
zip: str
@dataclass
class Person:
name: str
address: Address
p = Person("山田太郎", Address("東京", "100-0001"))
print(asdict(p))
{'name': '山田太郎', 'address': {'city': '東京', 'zip': '100-0001'}}
from dataclasses import dataclass, astuple
@dataclass
class Point:
x: int
y: int
p = Point(10, 20)
print(astuple(p))
(10, 20)
replace():オブジェクトを複製する
・役割: 既存のインスタンスから一部のフィールドを差し替えて新しいインスタンスを作成する。
・特徴::イミュータブル(frozen=True)でも安全に使える。
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Point:
x: int
y: int
p1 = Point(10, 20)
p2 = replace(p1, y=99)
print(p1) # Point(x=10, y=20)
print(p2) # Point(x=10, y=99)
型付きの辞書を定義するーTypeDict
まとまったデータを受け渡しするという意味では、データクラスとよく似た仕組みとして、TypeDict もある。
TypeDict:「辞書(dict)にどんなキーがあって、それぞれの値はどんな型なのか」を静的に指定できる
→ 実行時に辞書の中身を制限するものではなく、型チェッカー(mypy など)やIDEでの補完に役立つ
通常の辞書の場合
person = {"name": "山田太郎", "age": 32}
# どんなキー・値でも入ってしまう
person["age"] = "三十二" # 実行時はエラーにならない
TypeDict を用いた辞書
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
p: Person = {"name": "山田太郎", "age": 32}
p["age"] = "三十二" # 型チェッカーがエラーを指摘してくれる
→ 辞書ではあるが、クラスのようにキーの型を固定できる。
NotRequired と Required:キーが任意/必須か
・Required:明示的に「必須キーである」と指定する。
・Required:そのキーを省略できる(= 無くてもよい)
from typing import TypedDict, NotRequired, Required
class User(TypedDict):
id: Required[int]
name: str
email: NotRequired[str]
u1: User = {"id": 1, "name": "Alice"}
u2: User = {"id": 2, "name": "Bob", "email": "bob@example.com"} # どちらもOK
ReadOnly:特定のキーを読み取り専用にする
Python3.13では、特定のキーを読み取り専用にするための ReadOnly 型が追加された。
from typing import ReadOnly, TypeDict
class PersonDict(TypeDict):
name: ReadOnly[str]
age: int
married: bool
person: PersonDict = {'name': 'Yamada', 'age': 25, 'married': False}
person['name'] = 'Suzuki' # Error
@dataclass, NamedTuple, TypeDict のどれを用いるべきか
使い分けは、およそ以下のフローに沿って行えばよい。
・辞書(JSON)で来る/返す“構造化データ”のキー型を静的に縛りたい?
→ TypedDict( Required/NotRequired で必須/任意を明示)
・不変の軽量レコードを高速・小コストで扱いたい?(タプル互換・順序操作・アンパック)
→ NamedTuple
・ドメインモデルとしての“意味を持つ値オブジェクト”を丁寧に設計したい?
→ @dataclass(イミュータブルなら frozen=True, slots=True)
| 観点 | @dataclass |
NamedTuple |
TypedDict |
|---|---|---|---|
| 本体 | クラス(属性) | タプルのサブクラス | dict相当(型ヒント) |
| 既定の可変性 | 可変(frozen=Trueで不変) |
不変(immutable) | 可変(実行時は普通のdict) |
| メモリ効率/速度 | 良(slots=Trueでさらに良) |
とても良い(タプル準拠) | 普通(dictコスト) |
| 構文の分かりやすさ | 高い(明示的に属性名) | 高い(属性アクセス可) | 中(文字列キーアクセス) |
| ハッシュ可能性 |
frozen=Trueで自動hash |
既定でhash可 | dictなので不可(用途外) |
| デフォルト値 | あり(default/factory) |
あり(宣言時に指定) | あり(total=False/NotRequired等で省略制御) |
| ネスト構造の表現 | 容易 | 容易 | 容易(別TypedDict参照) |
| 継承 | 可能 | 可能(制約あり) | 基本非推奨(合成推奨) |
| 生成メソッド |
__init__/__repr__/__eq__自動 |
自動(タプル由来) | なし(型付けのみ) |
| 代表用途 | 値オブジェクト、ドメインモデル | 軽量レコード、固定形の返り値 | API/JSONスキーマの型注釈 |
| 実行時バリデーション | なし(型は静的) | なし | なし(型チェッカー依存) |
| パターンマッチ | 良(クラスパターン) | 良(シーケンス/クラス) | dictパターン |
参考文献
[1] 独習Python 第2版 (2025, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)