Pythonでデータを保持するクラスを定義する際、__init__、__repr__、__eq__などの定型コードを繰り返し書くのは面倒です。dataclassesモジュール(Python 3.7+)は、これらを自動生成し、クラス定義を劇的に簡潔にします。
従来のクラス vs dataclass
従来のアプローチ
class User:
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email
def __repr__(self) -> str:
return f"User(name={self.name!r}, age={self.age!r}, email={self.email!r})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, User):
return NotImplemented
return (self.name, self.age, self.email) == (other.name, other.age, other.email)
def __hash__(self) -> int:
return hash((self.name, self.age, self.email))
dataclassを使用
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str
わずか4行で、__init__、__repr__、__eq__が自動生成されます。
基本的な使い方
デフォルト値
from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
# 部分的に指定
config = Config(host="api.example.com")
print(config) # Config(host='api.example.com', port=8080, debug=False)
ミュータブルなデフォルト値
リストや辞書のようなミュータブルなデフォルト値にはfieldを使用:
from dataclasses import dataclass, field
@dataclass
class TodoList:
name: str
items: list[str] = field(default_factory=list)
# 各インスタンスが独立したリストを持つ
todo1 = TodoList("Work")
todo2 = TodoList("Home")
todo1.items.append("Meeting")
print(todo2.items) # [] - 影響を受けない
イミュータブルなdataclass
frozen=True
変更不可能なデータクラスを作成:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
point = Point(1.0, 2.0)
point.x = 3.0 # FrozenInstanceError!
# ハッシュ可能になる(辞書のキーやセットに使用可能)
points = {Point(0, 0), Point(1, 1)}
slots=True(Python 3.10+)
メモリ効率を向上:
@dataclass(slots=True)
class Coordinate:
x: float
y: float
z: float
# __slots__が自動生成され、メモリ使用量が削減
fieldオプション
よく使うfieldパラメータ
from dataclasses import dataclass, field
@dataclass
class Article:
title: str
content: str
# reprに含めない(長いテキスト向け)
body: str = field(repr=False)
# 比較に使用しない
view_count: int = field(default=0, compare=False)
# 初期化に含めない(後から設定)
id: int = field(init=False, default=0)
| パラメータ | 説明 | デフォルト |
|---|---|---|
default |
デフォルト値 | - |
default_factory |
デフォルト値を生成する関数 | - |
repr |
__repr__に含めるか |
True |
compare |
比較に使用するか | True |
hash |
ハッシュ計算に使用するか | None |
init |
__init__に含めるか |
True |
初期化後の処理
post_init
フィールドの検証や計算フィールドの設定:
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
def __post_init__(self) -> None:
if self.width <= 0 or self.height <= 0:
raise ValueError("Dimensions must be positive")
self.area = self.width * self.height
rect = Rectangle(3.0, 4.0)
print(rect.area) # 12.0
InitVar
初期化時のみ使用し、フィールドとして保持しない値:
from dataclasses import dataclass, field, InitVar
@dataclass
class User:
name: str
_password: str = field(init=False, repr=False)
password: InitVar[str] # __init__の引数だがフィールドではない
def __post_init__(self, password: str) -> None:
# パスワードをハッシュ化して保存
self._password = self._hash_password(password)
def _hash_password(self, password: str) -> str:
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
継承
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
@dataclass
class Employee(Person):
employee_id: str
department: str
emp = Employee("Alice", 30, "E001", "Engineering")
print(emp) # Employee(name='Alice', age=30, employee_id='E001', department='Engineering')
注意: デフォルト値の順序
@dataclass
class Base:
x: int = 0 # デフォルト値あり
@dataclass
class Derived(Base):
y: int # エラー!デフォルト値なしのフィールドが後に来ている
# 正しい方法
@dataclass
class Derived(Base):
y: int = 0 # デフォルト値を指定
dataclass vs NamedTuple vs TypedDict
比較
from dataclasses import dataclass
from typing import NamedTuple, TypedDict
# dataclass - 最も柔軟
@dataclass
class UserDC:
name: str
age: int
# NamedTuple - イミュータブル、タプルの機能
class UserNT(NamedTuple):
name: str
age: int
# TypedDict - 辞書の型定義
class UserTD(TypedDict):
name: str
age: int
| 特徴 | dataclass | NamedTuple | TypedDict |
|---|---|---|---|
| ミュータブル | 可(frozenで不可) | 不可 | 可 |
| インデックスアクセス | 不可 | 可 | 可 |
| メソッド追加 | 可 | 可 | 不可 |
| JSON互換性 | 変換必要 | 変換必要 | そのまま |
| メモリ効率 | 普通(slotsで向上) | 良い | 普通 |
使い分け
flowchart TD
Q1{"辞書として<br/>使いたい?"}
Q2{"イミュータブル<br/>が必要?"}
Q3{"メソッドを<br/>追加したい?"}
Q1 -->|Yes| TD["TypedDict"]
Q1 -->|No| Q2
Q2 -->|Yes| Q3
Q2 -->|No| DC1["dataclass"]
Q3 -->|Yes| DC2["dataclass<br/>(frozen=True)"]
Q3 -->|No| NT["NamedTuple"]
style TD fill:#f59e0b,color:#000
style DC1 fill:#3b82f6,color:#fff
style DC2 fill:#3b82f6,color:#fff
style NT fill:#22c55e,color:#fff
実践的なパターン
JSONシリアライズ
from dataclasses import dataclass, asdict, astuple
import json
@dataclass
class Product:
name: str
price: float
stock: int
product = Product("Widget", 29.99, 100)
# 辞書に変換
data = asdict(product)
print(json.dumps(data)) # {"name": "Widget", "price": 29.99, "stock": 100}
# タプルに変換
values = astuple(product)
print(values) # ('Widget', 29.99, 100)
ファクトリメソッド
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Event:
name: str
timestamp: datetime
source: str
@classmethod
def now(cls, name: str, source: str) -> "Event":
return cls(name, datetime.now(), source)
@classmethod
def from_dict(cls, data: dict) -> "Event":
return cls(
name=data["name"],
timestamp=datetime.fromisoformat(data["timestamp"]),
source=data["source"]
)
event = Event.now("user_login", "web")
設定クラス
from dataclasses import dataclass, field
from pathlib import Path
import os
@dataclass
class DatabaseConfig:
host: str = field(default_factory=lambda: os.getenv("DB_HOST", "localhost"))
port: int = field(default_factory=lambda: int(os.getenv("DB_PORT", "5432")))
database: str = field(default_factory=lambda: os.getenv("DB_NAME", "app"))
@property
def url(self) -> str:
return f"postgresql://{self.host}:{self.port}/{self.database}"
config = DatabaseConfig()
print(config.url)
Python 3.10+の新機能
match文との組み合わせ
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
def area(shape: Circle | Rectangle) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r * r
case Rectangle(width=w, height=h):
return w * h
kw_only(Python 3.10+)
@dataclass(kw_only=True)
class Config:
host: str
port: int
debug: bool = False
# 位置引数は使えない
config = Config(host="localhost", port=8080) # OK
config = Config("localhost", 8080) # エラー
まとめ
dataclassesは、データを保持するクラスの定義を大幅に簡素化します:
| オプション | 効果 |
|---|---|
frozen=True |
イミュータブル化、ハッシュ可能に |
slots=True |
メモリ効率向上 |
kw_only=True |
キーワード引数のみ許可 |
order=True |
比較演算子を自動生成 |
主要な原則:
- データクラスを優先: 単純なデータ保持には従来のクラスより簡潔
- frozenを活用: 変更不要なデータはイミュータブルに
- field()で制御: デフォルト値、repr除外、比較除外を適切に設定
- __post_init__で検証: 初期化後のバリデーションと計算
dataclassesは「設定より規約」の精神で、最小限のコードで最大の機能を提供します。