こんにちは、かつコーチです。
「データを保持するだけのクラスなのに、__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, 中級者向け, 設計