はじめに
dataclassはPython 3.7で導入された、データを保持するクラスを簡潔に書くための機能。
__init__と__repr__と__eq__を毎回書くの、いい加減だるくない?
Before(通常のクラス):
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
After(dataclass):
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
これだけで__init__、__repr__、__eq__が自動生成されます。
基本的な使い方
@dataclass
class User:
name: str
age: int = 0 # デフォルト値
active: bool = True
user = User("Alice")
print(user) # User(name='Alice', age=0, active=True)
デフォルト値の罠と解決策
❌ ダメな例
@dataclass
class BadExample:
items: list = [] # ValueError!
リストや辞書をデフォルト値にすると全インスタンスで共有されてしまうため、エラーになります。
✓ 正しい例
from dataclasses import dataclass, field
@dataclass
class GoodExample:
items: list = field(default_factory=list)
metadata: dict = field(default_factory=dict)
g1 = GoodExample()
g2 = GoodExample()
g1.items.append(1)
print(g1.items) # [1]
print(g2.items) # [] ← 影響なし!
frozen(イミュータブル)
@dataclass(frozen=True)
class ImmutablePoint:
x: float
y: float
ip = ImmutablePoint(1.0, 2.0)
ip.x = 3.0 # FrozenInstanceError!
# frozenだとハッシュ可能(dictのキーに使える)
print(hash(ip)) # -3550055125485641917
order(比較演算子)
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
versions = [
Version(2, 0, 0),
Version(1, 9, 5),
Version(1, 10, 0),
]
print(sorted(versions))
# [Version(1, 9, 5), Version(1, 10, 0), Version(2, 0, 0)]
フィールドの順番で比較されます(タプル比較と同じ)。
field() の詳細オプション
@dataclass
class Product:
name: str
price: float
_internal_id: str = field(default="", repr=False) # reprに含めない
created_at: str = field(default="", compare=False) # 比較に含めない
computed: float = field(init=False) # __init__に含めない
def __post_init__(self):
self.computed = self.price * 1.1 # 後処理で計算
p = Product("Widget", 100.0)
print(p) # Product(name='Widget', price=100.0, created_at='', computed=110.0)
| オプション | 説明 |
|---|---|
default |
デフォルト値 |
default_factory |
デフォルト値を返す関数 |
repr=False |
__repr__に含めない |
compare=False |
__eq__に含めない |
init=False |
__init__に含めない |
hash=None |
ハッシュ計算に含めるか |
post_init でバリデーション
@dataclass
class Email:
address: str
def __post_init__(self):
if "@" not in self.address:
raise ValueError(f"Invalid email: {self.address}")
Email("invalid") # ValueError!
Email("user@example.com") # OK
asdict / astuple
from dataclasses import asdict, astuple
import json
@dataclass
class Person:
name: str
age: int
person = Person("Alice", 30)
print(asdict(person)) # {'name': 'Alice', 'age': 30}
print(astuple(person)) # ('Alice', 30)
print(json.dumps(asdict(person))) # JSON出力
replace(イミュータブル更新)
from dataclasses import replace
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool
config = Config("localhost", 8080, False)
new_config = replace(config, debug=True)
print(config) # Config(host='localhost', port=8080, debug=False)
print(new_config) # Config(host='localhost', port=8080, debug=True)
Python 3.10+ の新機能
slots=True(メモリ効率向上)
@dataclass(slots=True)
class SlotPoint:
x: float
y: float
sp = SlotPoint(1.0, 2.0)
sp.z = 3.0 # AttributeError! 属性追加不可
-
__dict__がなくなりメモリ使用量が減る - 属性アクセスが高速に
kw_only=True(キーワード引数のみ)
@dataclass(kw_only=True)
class Config:
host: str
port: int
# Config("localhost", 8080) # TypeError!
Config(host="localhost", port=8080) # OK
パターンマッチ対応
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
def describe(shape):
match shape:
case Circle(radius=r) if r > 10:
return f"大きな円(半径{r})"
case Circle(radius=r):
return f"円(半径{r})"
case Rectangle(width=w, height=h):
return f"長方形({w}×{h})"
print(describe(Circle(5.0))) # 円(半径5.0)
print(describe(Circle(15.0))) # 大きな円(半径15.0)
継承
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
dog = Dog("Pochi", 3, "Shiba")
print(dog) # Dog(name='Pochi', age=3, breed='Shiba')
注意: 親クラスにデフォルト値があり、子クラスにデフォルト値がないとエラーになります。
ClassVar(クラス変数)
from typing import ClassVar
@dataclass
class Counter:
name: str
total_instances: ClassVar[int] = 0
def __post_init__(self):
Counter.total_instances += 1
Counter("A")
Counter("B")
print(Counter.total_instances) # 2
dataclass vs NamedTuple vs TypedDict
| 機能 | dataclass | NamedTuple | TypedDict |
|---|---|---|---|
| ミュータブル | ✓(デフォルト) | ✗ | ✓ |
| 継承 | ✓ | △ | ✓ |
| メソッド追加 | ✓ | ✓ | ✗ |
| パフォーマンス | 普通 | 良い | 良い |
| JSON変換 | asdict | _asdict | そのまま |
使い分け:
- dataclass: 一般的なデータ構造
- NamedTuple: イミュータブルでタプルとして使いたい
- TypedDict: 辞書として扱いたい(API応答など)
まとめ
# 基本形
@dataclass
class Simple:
field: type
# 推奨パターン
@dataclass(frozen=True, slots=True)
class Recommended:
field: type = field(default_factory=list)
def __post_init__(self):
# バリデーション
pass
ポイント:
- ミュータブルなデフォルト値は
field(default_factory=...) - イミュータブルにしたければ
frozen=True - 比較が必要なら
order=True - Python 3.10+なら
slots=Trueでメモリ効率UP
もう__init__を手書きする時代は終わりました。