【Python】dataclassの使い方の基本

Python でデータクラスを作成する際に便利な @dataclass デコレータの使い方について解説します。
目次
@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 を使って定義すると以下のようになります。
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 デコレータを使わない場合に比べて、定型的なコード(ボイラープレートコード)が減り、すっきりとした定義できています。
イミュータブル(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'
frozen=True を指定すると、x や y の値は定義後に変更できなくなります。例では、point1.x = 2 のように x の値を変更しようとしていますが例外となります。
dataclass の引数
@dataclass の frozen 引数を紹介しましたが、他にも多くの引数が存在します。詳細は公式ドキュメントのこちらを参照してください。
| 引数 | デフォルト | 概要 |
|---|---|---|
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 の場合、すべてのフィールドがキーワード引数専用になり、インスタンス化ではキーワード引数での指定が必要です。 |
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 を使用すれば、このリスクを回避しながらミュータブルなデフォルト値を安全に設定できます。
まとめ
Python でデータクラスを作成する際に便利な @dataclass デコレータの使い方について解説しました。
@dataclass デコレーターを使用すると、定型的なメソッド定義(ボイラープレートコード)を省略でき、データを表現するクラスの作成が非常に簡単になります。この記事では、簡単な例を使って @dataclass デコレータの使い方を紹介しました。
データクラス定義の際は、@dataclass デコレータの使用を検討してみてください。
上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

