Python: データ保持用のクラスを定義する (dataclasses)

DBのエンティティクラスを定義するときなど、データを持つことに特化した構造体のようなクラスが必要なケースがよくあります。

かつてはdict + typeやnamedtupleなどで用をなしてましたが、そういったクラスを簡単に定義できる標準ライブラリとして、Python 3.7からdataclassesが提供されてます。この使い方を整理していきます。

docs.python.org

$ python --version
Python 3.7.4

基本的な使い方としては、dataclassesをインポートして、クラスに@dataclasses.dataclassでアノテーションし、属性を列挙していくだけでOKです。
これだけでも素のクラスで定義した場合と比較して、__init__に代入式をつらつら書いていく作業から解放されます。

import dataclasses
from datetime import datetime

@dataclasses.dataclass
class User:
    name: str

u = User(name="田中 はじめ")
print(u)  # 田中 はじめ

もちろんクラスなので任意のメソッドを定義することもできます。

@dataclasses.dataclass
class User:
    name: str

    @property
    def family_name(self) -> str:
        return self.name.split(" ")[0]


u = User(name="田中 はじめ")
print(u.family_name)  # 田中

値 (フィールド) の入出力を詳細に制御するために、dataclasses.fieldを使います。

初期値をセットする場合、引数defaultに値を渡せばOKです。

class User:
    name: str
    email: str = dataclasses.field(default=None)

u = User(name="田中 はじめ")
print(u)  # User(name='田中 はじめ', email=None)

u = User(name="梅田 よしお", email="umeda@aaa.bbb.ccc")
print(u)  # User(name='梅田 よしお', email='umeda@aaa.bbb.ccc')

オブジェクトの生成日時 (created_at) のフィールドを持つことを考えてみます。

デフォルト値を自動で埋めるためには、default_factory引数に初期化関数 (ここではdatetime.now) を渡すことで実現できます。さらに生成時にセットさせないようにするには、init=Falseとすることで__init__の引数から除外することもできます。

@dataclasses.dataclass
class User:
    name: str
    created_at: datetime = dataclasses.field(default_factory=datetime.now, init=False)

u = User(name="田中 はじめ")
print(u)  # User(name='田中 はじめ', created_at=datetime.datetime(2020, 2, 1, 21, 39, 39, 311609))

u = User(name="梅田 よしお", created_at=datetime.now())
# -> TypeError: __init__() got an unexpected keyword argument 'created_at'

dataclassでは__init__後に__post_init__というメソッドが呼び出されます。これを実装することで、例えば他のフィールドの値から計算してセットされるフィールドを実現できます。
以下ではnameの値がデフォルト値となるnicknameを定義しています。(初期化されないようにinit=Falseとしてます。)

@dataclasses.dataclass
class User:
    name: str
    nickname: str = dataclasses.field(init=False)

    def __post_init__(self):
        self.nickname = self.name

u = User(name="田中 はじめ")
print(u)  # User(name='田中 はじめ', nickname='田中 はじめ')

継承もできます。Userを継承し、新たにbillingフィールドを追加したPremiumUserを作成してます。当然nameやemailも持ってます。

@dataclasses.dataclass
class User:
    name: str
    email: str = dataclasses.field(default=None)

@dataclasses.dataclass
class PremiumUser(User):
    billing: int = dataclasses.field(default=0)

u = PremiumUser(name="荒岩 かずみ")
print(u)  # PremiumUser(name='荒岩 かずみ', email=None, billing=0)

dataclassの引数でfrozen=True生成されたオブジェクトを変更不可 (イミュータブル) にできます。以下の場合、LeavedUserのnameは変更できません。

@dataclasses.dataclass(frozen=True)
class LeavedUser:
    name: str

u = LeavedUser(name="木村 ゆめこ")
u.name = "田中 ゆめこ"
# -> dataclasses.FrozenInstanceError: cannot assign to field 'name'

辞書やタプルなどの変換もデフォルトでサポートされてます。

u = User(name="田中 はじめ")

d = dataclasses.asdict(u)
print(d)  # {'name': '田中 はじめ', 'email': None}

t = dataclasses.astuple(u)
print(t)  # ('田中 はじめ', None)

簡単なデータの集合なのだけどクラスとしてまとめておきたい、というよくあるケースに広く対応できそうです。