🐍

【Python】冗長な__init__()定義を超単純にするdataclassesライブラリ

2022/05/24に公開

Pythonのコンストラクタの面倒 / 冗長 なコードを短くかけるdataclassesライブラリの紹介。

Pythonでクラスを定義するとき、以下のようなコードを書くことが多い

class Hoge:
    def __init__(self, attr1: int, attr2: float) -> None:
        self.attr1 = attr1
        self.attr2 = attr2

コンストラクタattr1attr2を引数に取り,そいつらを同名のアトリビュートに代入する。

これだけの操作なのに,同じ変数名を3回も書かなきゃいけないのがずっと面倒だった。

dataclassesを見つけるまでは。

dataclasses

公式ドキュメント

dataclassesはPythonの標準ライブラリであり、dataclassというデコレータを提供する。
このデコレータをクラス定義の前につけると、3回書く必要があったアトリビュート名が、以下のように1回で済む。

from dataclasses import dataclass

@dataclass
class HogeWithDC:
    attr1: int
    attr2: float

これら2つのクラスは、以下のように同じように動作する。

hoge = Hoge(1024, 3.1415)
print(hoge.attr1, hoge.attr2)
1024 3.1415
hoge_with_dc = HogeWithDC(1024, 3.1415)
print(hoge_with_dc.attr1, hoge_with_dc.attr2)
1024 3.1415

これは楽。

内部的には、このデコレータはHogeWithDCクラスに以下のような (最初に書いた冗長なやつと同じ) __init__()メソッドを自動で追加してくれるらしい。

def __init__(self, attr1: int, attr2: float) -> None:
    self.attr1 = attr1
    self.attr2 = attr2

いや、__init__()の中で変数に操作を加えたいんだけど...

できます。

全ての変数に操作を加える場合

全ての変数に操作を加えるなら、自分で__init__()を定義すればOK。 @dataclassで自動追加される__init__()メソッドは無視される。

from dataclasses import dataclass

@dataclass
class HogeWithInit:
    attr1: int
    attr2: float

    def __init__(self, attr1, attr2):
        self.attr1 = attr1**2
        self.attr2 = attr2 / 2

hoge = HogeWithInit(64, 2.718)
print(hoge.attr1, hoge.attr2)
4096 1.359

自分で定義した__init__()の通りにアトリビュートが登録されていることがわかる。

これならそもそも@dataclass使う必要ないんじゃないの?って思うかもしれませんが、@dataclassには他にもいいことがありますので、こういう使い方をする場合もあると思います。
この"他のいいこと"は後で話します。

一部の変数にのみ操作を加えたい場合

一部の変数にのみ操作を加えたい場合は、__post_init__()という特殊メソッドを定義し、その中に加えたい操作を書けば良い。

@dataclassデコレータをつけたクラスでは、(自分で__init__()を定義していない場合に限り) __init__()実行後に__post_init__()が自動で実行される。

これを使えば、@dataclassで自動追加される__init__()メソッドで定義されたアトリビュートを使って、自分でアトリビュートを更新、変更できる。

@dataclass
class HogeWithPostInit:
    attr1: int
    attr2: float

    def __post_init__(self):
        self.attr2 = self.attr2 / 2 # アトリビュートを変更
        self.attr3: float = self.attr1 * self.attr2 # アトリビュートを追加


hoge = HogeWithPostInit(64, 4)
print(hoge.attr1, hoge.attr2, hoge.attr3)
64 2.0 128.0

@dataclassがやることはinitメソッド追加だけじゃない

参考:pythonクラスの特殊メソッド一覧

@dataclassデコレータは、__init__()メソッド以外にも以下の特殊メソッドを自動で定義してくれる。

__repr__(): repr()関数( + print()関数)で返される文字列を定義

自作クラスを定義した際、__str__()または__repr__()メソッドを定義しない限り、その自作クラスのインスタンスをprint()やrepr()に渡しても、あまり意味のない文字列しか返さない。

class Hoge:
    def __init__(self, attr1: int, attr2: float) -> None:
        self.attr1 = attr1
        self.attr2 = attr2

hoge = Hoge(1024, 3.1415)
print(hoge)
<__main__.Hoge object at 0x7fae2079c730>

しかし、@dataclassをつけてクラスを定義すれば、__repr__()メソッドをいい感じに自動追加してくれるため、そのインスタンスを説明する意味のある文字列を返してくれる。

from dataclasses import dataclass

@dataclass
class HogeWithDC:
    attr1: int
    attr2: float

hoge_with_dc = HogeWithDC(1024, 3.1415)
print(hoge_with_dc)
HogeWithDC(attr1=1024, attr2=3.1415)

__eq__(), __ne__(): ==, !=で比較可能にする

自作クラスのインスタンス同士を等号で比較した場合、デフォルトでは全く同じインスタンスでない限りエラーになる。

具体的には、以下のように全く同じ値で2つのインスタンスを作っても、デフォルトではこれらは等しくないと判定される。

hoge1 = Hoge(1,5)
hoge2 = Hoge(1,5)

print(hoge1 == hoge2)
False

@dataclassをつけてクラスを定義すれば、==, !=で比較するときに呼び出される特殊メソッド__eq__(), __ne__()をいい感じに自動追加してくれるため、
同じ値で作った2インスタンスは等しいと判定される。一般的にはこちらの方が望ましい挙動であろう。

hoge_dc1 = HogeWithDC(1,5)
hoge_dc2 = HogeWithDC(1,5)
print(hoge_dc1 == hoge_dc2)
True

内部的には、比較する2つのインスタンスのそれぞれに対して、そのインスタンスのアトリビュートを全て並べたタプルを作り、そのタプル同士を比較しているらしい。

すなわち、以下の2つの式が等価。

hoge_dc1 == hoge_dc2
(hoge_dc1.attr1, hoge_dc1.attr2) == (hoge_dc2.attr1, hoge_dc2.attr2)
True

これ以外の方法で等式評価をしたい場合には、自分で__eq__(), __ne__()を実装する必要あり。

オプションを指定することで、不等式での評価を可能にするなどさらに便利に使えそうだが、それはまた今度まとめます。

おわりに

こんな便利な機能が標準ライブラリにあったとは。
標準ライブラリのドキュメントはしっかり漁らなければ。

Discussion