dataclasses

【Python】dataclassの使い方の基本

【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 

上記例では、xyの値を持つような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__()で定義するようにしましょう。

Note

@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'

上記のようにすると、xyの値は定義後に変更できなくなります。例では、point1.x=2のようにxの値を変更しようとしていますが、例外となります。

dataclassの引数

@dataclassfrozen引数を紹介しましたが、他にも多くの引数が存在します。詳細は公式ドキュメントのこちらを参照してください。

※本記事執筆時点の3.11のドキュメントを参考にしているため、最新のPythonバージョンでは変更されている可能性があります。

引数デフォルト概要
initTrue__init__()メソッドを生成します。
reprTrue__repr__()メソッドを生成します。
eqTrue__eq__()メソッドを生成します。
orderFalseTrueに設定するとインスタンスを比較するためのメソッド(__lt__(), __le__(), __gt__(), __ge__())が生成されます。
unsafe_hashFalseTrueに設定すると__hash__()メソッドが生成され、インスタンスをハッシュ可能にします。
frozenFalseTrueに設定するとクラスを変更できないimmutableにします。
match_argsTrue__match_args____init__に渡されたパラメータのリストから作成されます。
kw_onlyFalseTrueの場合、すべてのフィールドがキーワード専用になります。この場合、__init__呼び出し時にキーワード引数で指定しなければならなくなります。
slotsFalseTrueにすると__slots__が作成されます。
weakref_slotFalseTrueにすると__weakref__が作成されます。

これまでの例でinit引数を指定しなくても__init__()が生成されていたのは、該当するinit引数のデフォルトがTrueであるためです。デフォルトでFalseになっている項目は、@dataclassの引数でTrueを指定することで有効にすることができます。

必要に応じて上記の引数の設定を変更して利用してください。

デフォルト値の割り当て

@dataclassデコレータを使ってデータクラスを定義する際に、デフォルト値を割り当てたい場合は、以下のようにfield関数を使います。特に、ミュータブルな型のデフォルト値を安全に設定する場合には、fielddefault_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という変数のデフォルト値は""(空文字)としています。イミュータブルな型のデフォルト値は、fielddefault引数または直接割り当て(例: 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を学習を進めている方は、関数やクラスのデフォルト引数としてミュータブルな型を使用する際のリスクについて学んだことがあるかと思います。複数のインスタンスが同じミュータブルなオブジェクトを共有してしまうと、予期しない問題が発生するのですが、@dataclassfieldを使用すれば、このリスクを回避しながらミュータブルなデフォルト値を安全に設定できます。

上記の例でも、リストにset_dataメソッドで値を追加していますが、test1test2のインスタンス間でmutableリストの内容が混ざることはありません。

まとめ

Pythonでデータクラスを作成する際に便利な@dataclassデコレータの使い方について解説しました。

@dataclassデコレーターを使用すると、定型的なメソッド定義(ボイラープレートコードとも言われる)を省略でき、データを表現するクラスの作成が非常に簡単になります。本記事では、簡単な例を使って@dataclassデコレータの使い方を紹介しました。

@dataclassデコレータは非常に便利ですが、データの保持が主目的としたクラスの定義に特に適しています。それ以外のケースや、初期化に複雑な処理が必要な場合には、標準的な__init__()を用いたコンストラクタの定義が推奨されます。

データクラスの定義の際には、@dataclassデコレータの使用を是非検討してみてください。

Note

dataclassesの公式ドキュメントはこちらを参照してください。