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 

上記例では、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__()でコンストラクタを使って定義するようにしましょう。

Note

dataclassクラスデコレータで作成されるメソッドを調べたい場合は、helpを使うと確認できます。例えば、上記例でpoint1を定義した後に、「help(point1)」というようにすると以下のように表示されます。

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を紹介しましたが、dataclassの引数は他にもあるので一覧で紹介します。詳細は公式ドキュメントのこちらを参照してください。

※本記事執筆時点の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を使って定義します。

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という変数はstrで、デフォルト値は””(空文字)としています。デフォルト値はfieldのdefault引数で指定できます。

なお、変更不可(イミュータブル)な型の定義をする場合には、fieldを使わずに以下のように定義することも可能です。

immutable: str = ""

一方で、変更可能(ミュータブル)なlist等を使う場合には「mutable: list = []」というような定義はできません。定義しようとすると以下のようにValueErrorとなります。

ValueError: mutable default <class 'list'> for field mutable is not allowed: use default_factory

変更可能(ミュータブル)なlist等を使う場合は「mutable: list = field(default_factory=list)」というように、default_factory引数で型を指定します。

Pythonを勉強している方であれば、変更可能(ミュータブル)な型をデフォルト値にすることは注意が必要ということを学んだことがある人が多いと思います。ミュータブルな変数は、複数インスタンスが共有してしまうことにより、想定外の問題が発生することがあるですが、fieldを使用すればインスタンス間で状態が漏れるようなリスクなく使用することができます。

上記の例でも、リストにset_dataメソッドで値を追加していますが、test1とtest2のインスタンス間でmutableというリストの内容が混ざってしまうようなことは起こっていないことが分かります。

まとめ

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

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

dataclassクラスデコレータは便利ですが、データの保持が主な目的のクラスに対する利用に限定するのが適切で、データを表現するクラスではない場合や、初期化に複雑な処理が必要な場合は一般的な__init__()でコンストラクタを使って定義するのがいいでしょう。

データを表現するクラスでは、dataclassの使用を是非検討してみてください。

Note

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