Pythonのdataclassで引数が属性に自動反映される理由
@dataclassを付けたクラスは、Point(10, 20)と書くだけでxとyに値が入ります。self.x = xのような代入を書いた覚えがないのに、なぜ動くのでしょうか。
結論から言うと、@dataclassがクラスに書かれた属性の宣言を読み取り、__init__メソッドを自動で作っているからです。この記事では、その仕組みと、手書きの初期化コードを減らす方法を解説します。
こんな悩みを解決します
- コンストラクタに値を渡しただけで属性に入る理由への疑問
- 属性が増えるたびに
__init__とself.xxx = xxxを書く手間 @dataclassの裏側で起きていることの把握
@dataclassとは何か
Python 3.7以降の標準ライブラリdataclassesが提供するデコレータです。デコレータとは、クラスや関数の直前に@名前と書いて、機能を付け足す仕組みです。
クラスに@dataclassを付けると、次のようなメソッドが自動生成されます。
__init__(コンストラクタ)__repr__(文字列表現)__eq__(比較演算子)- その他のメソッド(必要に応じて)
毎回同じように書くボイラープレート(定型)コードが減るため、読みやすく保守しやすいクラスになります。
なぜコンストラクタに渡した値が自動で属性にセットされるのか
次のクラスを例にします。
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
このクラスで
p = Point(10, 20)
とすると、p.xは10、p.yは20になります。
x: intは「属性名: 型」の宣言で、型ヒント(アノテーション)と呼ばれます。@dataclassは、クラスが定義される時点でこの宣言を上から順に読み取り、同じ名前の引数を持つ__init__を作ります。生成されるコードは、次の手書きと同じ内容です。
def __init__(self, x: int, y: int):
self.x = x
self.y = y
引数は宣言した順にx、yへ割り当てられます。Point(y=20, x=10)のように名前を指定して渡すこともできます。
型ヒントは実行時にチェックされない
x: intと書いても、型の検証は行われません。Point("a", "b")のように文字列を渡してもエラーにならず、そのまま属性に入ります。型の誤りを見つけたい場合は、mypyなどの静的型チェッカーを併用します。
手書きの__init__と比べて何が変わるか
手動での__init__作成の手間を省ける
従来は、次のように書く必要がありました。
class Point:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
属性が10個あれば、引数の宣言と代入がそれぞれ10行必要です。@dataclassなら、属性を1行ずつ宣言するだけで済みます。
コードの可読性が向上する
属性の宣言だけで初期化コードまで揃うため、クラスの定義を見れば持っているデータの一覧がそのまま分かります。
バグを減らせる
手書きではself.x = yのような代入ミスが起こりえます。自動生成なら、宣言と代入がずれる心配がありません。
__init__の後に独自の処理を足したい場合
値のチェックや派生値の計算など、初期化の直後に処理を入れたい場合は__post_init__メソッドを使います。自動生成された__init__が属性への代入を終えた直後に呼ばれます。
@dataclass
class Point:
x: int
y: int
def __post_init__(self):
print(f'Pointが作成されました: ({self.x}, {self.y})')
このPointをPoint(10, 20)で作ると、「Pointが作成されました: (10, 20)」と表示されます。__init__自体を書き直す必要はありません。
まとめ
@dataclassによる属性の宣言からの__init__自動生成と、渡した引数の属性への反映- 引数と宣言順の対応、および型ヒントが実行時にはチェックされない点
- 初期化コードの手書き不要化による、コードの短縮とバグの削減
__post_init__による初期化後の処理の追加
