DBのエンティティクラスを定義するときなど、データを持つことに特化した構造体のようなクラスが必要なケースがよくあります。
かつてはdict + typeやnamedtupleなどで用をなしてましたが、そういったクラスを簡単に定義できる標準ライブラリとして、Python 3.7からdataclassesが提供されてます。この使い方を整理していきます。
$ 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)
簡単なデータの集合なのだけどクラスとしてまとめておきたい、というよくあるケースに広く対応できそうです。