【Python】dataclassでシンプルにクラスを定義する

Python

こんにちは、かつコーチです。

「データを保持するだけのクラスなのに、__init____repr__を毎回手書きしている」

そんな面倒さを感じたことはないでしょうか。

Pythonには、そうした定型コードを自動生成してくれるdataclassという仕組みがあります。

この記事では、@dataclassデコレータの基本、通常のクラス定義との比較、そしてfrozen=Trueによるイミュータブル化までを解説します。

読み終える頃には、データ保持用のクラスをすっきり書けるようになります。

基本の書き方・実装手順

手順1:dataclassをimportする

dataclassesは標準ライブラリなので、追加インストールは不要です。

from dataclasses import dataclass

手順2:@dataclassデコレータでクラスを定義する

商品情報を表すProductクラスを作ります。

from dataclasses import dataclass


@dataclass
class Product:
    name: str
    price: int
    stock: int = 0

型ヒント付きでフィールドを並べるだけで、__init__メソッドが自動生成されます。

手順3:インスタンスを作成して動作を確認する

product = Product(name="キーボード", price=8000)
print(product)
Product(name='キーボード', price=8000, stock=0)

__repr__も自動生成されているため、print()しただけでフィールドの中身が見やすく表示されます。

手順4:等価比較も自動で使えることを確認する

p1 = Product(name="マウス", price=3000)
p2 = Product(name="マウス", price=3000)
print(p1 == p2)
True

通常のクラスでは==はデフォルトでオブジェクトのIDを比較しますが、dataclassはフィールドの値同士を比較してくれます。

通常のクラス定義(initを自分で書く場合)との比較

同じProductクラスを、通常のクラス定義で書くと次のようになります。

class ProductManual:
    def __init__(self, name: str, price: int, stock: int = 0):
        self.name = name
        self.price = price
        self.stock = stock

    def __repr__(self):
        return f"ProductManual(name={self.name!r}, price={self.price!r}, stock={self.stock!r})"

    def __eq__(self, other):
        if not isinstance(other, ProductManual):
            return NotImplemented
        return (self.name, self.price, self.stock) == (other.name, other.price, other.stock)

フィールドが3つ増えるたびに、__init____repr____eq__のすべてに手を入れる必要があります。

dataclassなら、フィールドの型ヒントを1行追加するだけで済みます。

項目通常のクラスdataclass
__init__自分で書く自動生成
__repr__自分で書く自動生成
__eq__自分で書く自動生成
フィールド追加時の変更箇所複数メソッド型ヒント1行
メソッドを独自定義したい場合自由通常通り追加可能

データを保持するだけのクラスであれば、dataclassを使うことでコード量を大きく減らせます。

よくあるつまずきポイント・エラー対処

frozen=Trueを知らずミュータブルなまま値を書き換えてしまう

筆者は以前、設定値を保持するConfigクラスをdataclassで作ったとき、意図せず途中で値を書き換えてしまいバグを起こした経験があります。

❌ Before(通常のdataclass、値が書き換え放題)

from dataclasses import dataclass


@dataclass
class Config:
    api_key: str
    timeout: int = 30


config = Config(api_key="abc123")

# どこか別の処理で、うっかり上書きしてしまう
config.timeout = -1
print(config.timeout)
-1

timeoutにマイナス値が入ってもエラーにならず、そのまま処理が進んでしまいました。

原因は、通常のdataclassのフィールドがミュータブル(変更可能)だったことです。

✅ After(frozen=Trueでイミュータブル化)

from dataclasses import dataclass


@dataclass(frozen=True)
class Config:
    api_key: str
    timeout: int = 30


config = Config(api_key="abc123")
config.timeout = -1
Traceback (most recent call last):
  File "config_test.py", line 10, in <module>
    config.timeout = -1
dataclasses.FrozenInstanceError: cannot assign to field 'timeout'

frozen=Trueを付けると、インスタンス生成後のフィールド変更がFrozenInstanceErrorとして拒否されます。

設定値のように「一度作ったら変わってほしくない」データには、frozen=Trueを付けるのがおすすめです。

意図しない書き換えバグを、実行時のエラーとして早期に検知できるようになります。

デフォルト値にミュータブルなオブジェクトを直接書いてしまう

もう1つよくあるのが、リストや辞書をデフォルト値にそのまま書いてしまうケースです。

❌ Before(リストを直接デフォルト値にする)

from dataclasses import dataclass


@dataclass
class Cart:
    items: list = []
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory

dataclassは、ミュータブルなデフォルト値をあえてエラーにして防いでくれます。

✅ After(default_factoryを使う)

from dataclasses import dataclass, field


@dataclass
class Cart:
    items: list = field(default_factory=list)


cart1 = Cart()
cart2 = Cart()
cart1.items.append("りんご")
print(cart1.items, cart2.items)
['りんご'] []

field(default_factory=list)を使うと、インスタンスごとに新しいリストが作られます。

これにより、複数のインスタンスが同じリストを共有してしまう事故を防げます。

まとめ

この記事のポイント

  • @dataclassは、__init____repr____eq__を型ヒントから自動生成する
  • 通常のクラス定義と比べて、フィールド追加時の変更箇所が大きく減る
  • frozen=Trueを付けると、生成後のフィールド変更がFrozenInstanceErrorになる
  • 設定値など不変であるべきデータにはfrozen=Trueが有効
  • リストや辞書のデフォルト値はfield(default_factory=list)を使う

次に読むべき記事

  • 循環importでハマった話と解決法
  • PEP8に沿ったコードスタイルとlinter(Ruff)の使い方
  • 型ヒント(Type Hints)の書き方入門

タグ: Python, 中級者向け, 設計

タイトルとURLをコピーしました