7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

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__を手書きする時代は終わりました。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?