Pythonでデータクラスを作成する際に便利な@dataclass
デコレータの使い方について解説します。
Contents
@dataclassデコレーター
Pythonのクラスを作成する際には、通常、インスタンス生成時の初期化のための__init__()
メソッドを定義します。クラス定義については「クラスの定義と使い方」でまとめていますので興味があれば参考にしてください。
プログラミングでは、データを表現するだけのためにクラスを使う場合があります。例えば、2次元の位置を表すような点を表現したいような場合です。このようなケースでは、@dataclass
デコレータを使用すると、冗長な初期化や表現メソッドを書くことなく、簡潔にクラスを定義できます。
本記事では、簡単なデータクラスの定義例を使いながら@dataclass
デコレータの使い方の基本を紹介します。
一般的なデータクラスの定義方法
簡単な例として、2次元の点を表すPointクラスを@dataclass
デコレータを使わない一般的な方法で作成してみます。これにより@dataclass
を使った時にどういった点が省略されて楽になるかが分かりやすいかと思います。@dataclass
デコレータの使い方を知りたいだけという場合は、読み飛ばしてもらって構いません。
一般的なデータクラスの定義方法は、以下のようになります。
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 __add__(self, other): """+演算子""" return Point(self.x + other.x, self.y + other.y) def __sub__(self, other): """-演算子""" return Point(self.x - other.x, self.y - other.y) def __eq__(self, other): """等価比較""" return self.x == other.x and self.y == other.y if __name__ == "__main__": point1 = Point(1, 1) point2 = Point(2, 2) point3 = Point(1, 1) print(point1) print(point2) print(point1 + point2) print(point1 - point2) print(point1 == point3)
【実行結果】 Point(x=1, y=1) Point(x=2, y=2) Point(x=3, y=3) Point(x=-1, y=-1) True
上記例では、x
とy
の値を持つようなPoint
クラスを定義しています。Point
クラス内では、以下のようなメソッドを定義しています。
メソッド | 内容 |
---|---|
__init__() | インスタンスを初期化するコンストラクタ |
__repr__() | インスタンスがprint 文に渡された際の表示形式を定義 |
__add__() | Point 同士の加算を定義 |
__sub__() | Point 同士の減算を定義 |
__eq__() | Point 同士の等価比較する |
上記のようなメソッドを定義をすることで、Point
というデータのインスタンスを作成し、加算・減算したり、等価比較したりすることができるようになります。
データ専用クラスの場合は、上記のようにコンストラクタや文字列表現、等価比較等の定型的なコード(ボイラープレートコード)を書く必要があります。@dataclass
デコレータを使用することで、こういった定型コードの作成を省略することができます。
@dataclassデコレータの使い方
以降では、@dataclass
デコレータの使い方を紹介していきます。
基本的な使い方
上記で作成した2次元の点を表すPoint
クラスを@dataclass
を使って作成します。@dataclass
デコレータを使って定義する場合は以下のようにします。
from dataclasses import dataclass @dataclass class Point: """位置クラス""" x: int y: int def __add__(self, other): """+演算子""" return Point(self.x + other.x, self.y + other.y) def __sub__(self, other): """-演算子""" return Point(self.x - other.x, self.y - other.y) if __name__ == "__main__": point1 = Point(1, 1) point2 = Point(2, 2) point3 = Point(1, 1) print(point1) print(point2) print(point1 + point2) print(point1 - point2) print(point1 == point3)
【実行結果】 Point(x=1, y=1) Point(x=2, y=2) Point(x=3, y=3) Point(x=-1, y=-1) True
@dataclass
デコレータを使いたい場合は、上記例のようにクラス定義で「@dataclass
」をつけます。
@dataclass
デコレータをクラス定義に付与すると、Point
の属性のアノテーションを読み込み、自動で__init__()
、__repr__()
、__eq__()
メソッドを生成してくれます。__eq__()
メソッドはインスタンスの全ての属性が等しいかどうかで等価判定します。なお、__add__()
や__sub__()
は自動生成されないので個別に定義しています。
@dataclass
デコレータを使わない場合に比べて、定型的なコード(ボイラープレートコード)が減り、すっきりと定義できていることが分かるかと思います。
@dataclass
デコレータは便利ですが、データの保持が主な目的のクラスに対する利用に限定するのが適切です。データ専用のクラスではない場合や、初期化に複雑な処理が必要な場合は、一般的な__init__()
で定義するようにしましょう。
@dataclass
デコレータで作成されるメソッドを調べたい場合は、helpを使うと確認できます。例えば、クラスを定義した後に「help(Point)
」というようにすると以下のように表示されます。
Help on Point in module __main__ object: class Point(builtins.object) | Point(x: int, y: int) -> None | | 位置クラス | | Methods defined here: | | __add__(self, other) | +演算子 | | __eq__(self, other) | Return self==value. | | __init__(self, x: int, y: int) -> None | Initialize self. See help(type(self)) for accurate signature. | | __repr__(self) | Return repr(self). | | __sub__(self, other) | -演算子 | | ---------------------------------------------------------------------- | Data descriptors defined here: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined) | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __annotations__ = {'x': <class 'int'>, 'y': <class 'int'>} | | __dataclass_fields__ = {'x': Field(name='x',type=<class 'int'>,default... | | __dataclass_params__ = _DataclassParams(init=True,repr=True,eq=True,or... | | __hash__ = None | | __match_args__ = ('x', 'y')
__init__()
メソッドや、__eq__()
メソッド、__repr__()
メソッド等が定義されていることが分かるかと思います。
イミュータブル(immutable)なデータクラスを定義する場合 frozen
@dataclass
デコレータは、他にも便利な機能があります。例えば、ハッシュ可能として辞書のキーに使ったり、集合に追加するためにPoint
を変更不可(immutable)として定義したくなることがあります。
この場合には、以下のようにfrozen=True
という引数を@dataclass
デコレータの引数に追加するだけで簡単に実現できます。
from dataclasses import dataclass @dataclass(frozen=True) class FrozenPoint: """位置クラス""" x: int y: int def __add__(self, other): """+演算子""" return FrozenPoint(self.x + other.x, self.y + other.y) def __sub__(self, other): """-演算子""" return FrozenPoint(self.x - other.x, self.y - other.y) if __name__ == "__main__": point1 = FrozenPoint(1, 1) point1.x = 2 print(point1)
【実行結果】 Traceback (most recent call last): ...(省略)... dataclasses.FrozenInstanceError: cannot assign to field 'x'
上記のようにすると、x
やy
の値は定義後に変更できなくなります。例では、point1.x=2
のようにx
の値を変更しようとしていますが、例外となります。
dataclassの引数
@dataclass
のfrozen
引数を紹介しましたが、他にも多くの引数が存在します。詳細は公式ドキュメントのこちらを参照してください。
※本記事執筆時点の3.11のドキュメントを参考にしているため、最新のPythonバージョンでは変更されている可能性があります。
引数 | デフォルト | 概要 |
---|---|---|
init | True | __init__() メソッドを生成します。 |
repr | True | __repr__() メソッドを生成します。 |
eq | True | __eq__() メソッドを生成します。 |
order | False | True に設定するとインスタンスを比較するためのメソッド(__lt__() , __le__() , __gt__() , __ge__() )が生成されます。 |
unsafe_hash | False | True に設定すると__hash__() メソッドが生成され、インスタンスをハッシュ可能にします。 |
frozen | False | True に設定するとクラスを変更できないimmutableにします。 |
match_args | True | __match_args__ が__init__ に渡されたパラメータのリストから作成されます。 |
kw_only | False | True の場合、すべてのフィールドがキーワード専用になります。この場合、__init__ 呼び出し時にキーワード引数で指定しなければならなくなります。 |
slots | False | True にすると__slots__ が作成されます。 |
weakref_slot | False | True にすると__weakref__ が作成されます。 |
これまでの例でinit
引数を指定しなくても__init__()
が生成されていたのは、該当するinit
引数のデフォルトがTrue
であるためです。デフォルトでFalse
になっている項目は、@dataclass
の引数でTrue
を指定することで有効にすることができます。
必要に応じて上記の引数の設定を変更して利用してください。
デフォルト値の割り当て
@dataclass
デコレータを使ってデータクラスを定義する際に、デフォルト値を割り当てたい場合は、以下のようにfield
関数を使います。特に、ミュータブルな型のデフォルト値を安全に設定する場合には、field
のdefault_factory
引数を利用します。
from dataclasses import dataclass, field @dataclass class DataWithDefaults: """デフォルト値の設定""" immutable: str = field(default="") mutable: list = field(default_factory=list) def set_data(self, value): self.immutable = value self.mutable.append(value) if __name__ == "__main__": test1 = DataWithDefaults() test2 = DataWithDefaults() test1.set_data("data1-1") test1.set_data("data1-2") test2.set_data("data2-1") # インスタンスの属性を確認 print(test1.immutable) print(test1.mutable) print(test2.immutable) print(test2.mutable)
【実行結果】 data1-2 ['data1-1', 'data1-2'] data2-1 ['data2-1']
上記例では、immutable
という変数のデフォルト値は""
(空文字)としています。イミュータブルな型のデフォルト値は、field
のdefault
引数または直接割り当て(例: immutable: str = ""
)で指定できます。
しかし、ミュータブルな型のデフォルト値、例えば「mutable: list = []
」といった直接の割り当てはエラーとなります。具体的には、以下のようなValueError
となります。
ValueError: mutable default <class 'list'> for field mutable is not allowed: use default_factory
この問題を回避するためには「mutable: list = field(default_factory=list)
」というようにdefault_factory
引数を使用します。
Pythonを学習を進めている方は、関数やクラスのデフォルト引数としてミュータブルな型を使用する際のリスクについて学んだことがあるかと思います。複数のインスタンスが同じミュータブルなオブジェクトを共有してしまうと、予期しない問題が発生するのですが、@dataclass
のfield
を使用すれば、このリスクを回避しながらミュータブルなデフォルト値を安全に設定できます。
上記の例でも、リストにset_data
メソッドで値を追加していますが、test1
とtest2
のインスタンス間でmutable
リストの内容が混ざることはありません。
まとめ
Pythonでデータクラスを作成する際に便利な@dataclass
デコレータの使い方について解説しました。
@dataclass
デコレーターを使用すると、定型的なメソッド定義(ボイラープレートコードとも言われる)を省略でき、データを表現するクラスの作成が非常に簡単になります。本記事では、簡単な例を使って@dataclass
デコレータの使い方を紹介しました。
@dataclass
デコレータは非常に便利ですが、データの保持が主目的としたクラスの定義に特に適しています。それ以外のケースや、初期化に複雑な処理が必要な場合には、標準的な__init__()
を用いたコンストラクタの定義が推奨されます。
データクラスの定義の際には、@dataclass
デコレータの使用を是非検討してみてください。
dataclassesの公式ドキュメントはこちらを参照してください。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。