dataclasses

【Python】dataclassの使い方の基本

【Python】dataclassの使い方の基本
naoki-hn

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 を使って定義すると以下のようになります。

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 デコレータで作成されるメソッドを調べたい場合は、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'

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

dataclass の引数

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

引数デフォルト概要
initTrue__init__() メソッドを生成します。
reprTrue__repr__() メソッドを生成します。
eqTrue__eq__() メソッドを生成します。
orderFalseTrue に設定するとインスタンスを比較するためのメソッド (__lt__(), __le__(), __gt__(), __ge__()) が生成されます。
unsafe_hashFalseTrue に設定すると __hash__() メソッドが生成され、インスタンスをハッシュ可能にします。
frozenFalseTrue に設定するとクラスを変更できない immutable にし、ハッシュ可能になります。
match_argsTrue__match_args____init__ に渡されたパラメータリストから作成されます。
kw_onlyFalseTrue の場合、すべてのフィールドがキーワード引数専用になり、インスタンス化ではキーワード引数での指定が必要です。
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 を使用すれば、このリスクを回避しながらミュータブルなデフォルト値を安全に設定できます。

まとめ

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

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

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

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

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
【Python Tech】プログラミングガイド
【Python Tech】プログラミングガイド
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。 私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

プログラミング言語はこれまでC、C++、JAVA等を扱ってきましたが、最近では特に機械学習等の分析でも注目されているPythonについてとても興味をもって取り組んでいます。これまでの経験をもとに、Pythonに興味を持つ方のお役に立てるような情報を発信していきたいと思います。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました