【Python】冗長な__init__()定義を超単純にするdataclassesライブラリ
Pythonのコンストラクタの面倒 / 冗長 なコードを短くかけるdataclasses
ライブラリの紹介。
Pythonでクラスを定義するとき、以下のようなコードを書くことが多い
class Hoge:
def __init__(self, attr1: int, attr2: float) -> None:
self.attr1 = attr1
self.attr2 = attr2
コンストラクタattr1
とattr2
を引数に取り,そいつらを同名のアトリビュートに代入する。
これだけの操作なのに,同じ変数名を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メソッド追加だけじゃない
@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