Python実践入門 ── 言語の力を引き出し、開発効率を高める (WEB+DB PRESS plusシリーズ)
目次
はじめに
C++ユーザやJuliaユーザがPythonを使っている時の不満の一つとして、
データを格納する目的のstructが無いことが上げられます。
もちろんPythonのclassを使って、データのみを格納することもできますが、
記述が冗長になりますし、コールドリーディングしているときに、
データを格納するためのclassなのか、
それともより汎用的なクラスなのかが、一見してわかりにくいことがあります。
そこでPython3.7から導入されたのが、dataclassです。
これを使えば、Pythonでも明示的にデータ格納用のクラスを簡単に実装できます。
また、下記のツイートの通り、アプリケーションの設定データ管理として、
このdataclassを使うと非常に便利です。
今回のPFN技術ブログすばらしい。"JSONを読み込んだDict[str, Any]をそのまま設定としてアプリの中で使い回すのはやめよう。dataclassなどを使って、型付きクラスにして、最初にチェックしよう" 心が痛い。 Practices for Working with Configuration in Python Applications https://t.co/ru3fAGixSL
— Atsushi Sakai (@Atsushi_twi) 2020年4月25日
今回の記事では、このdataclassの概要について説明したいと思います。
通常のclassとdataclassの比較
通常、データを格納するクラスを作成する場合、
class Hoge: def __init__(self, x=0, y=0, name=""): self.x = x self.y = y self.name = name
のようになりますが、dataclassを使った場合、
@dataclass class Hoge2: x: int = 0 y: int = 0 name: str = ""
のように書けます。
使い方は基本的に一緒です。
hoge = Hoge() hoge2 = Hoge2()
dataclassの良いところ
通常のclassでデータ格納用のクラスを作った時と、
dataclassを使った時を比較して、dataclassの良いところをまとめておきます。
データを格納する箱であることを明確にできる。
個人的にこれが一番重要だと思いますが、
クラスの上に@dataclassと書くことで、
そのクラスがデータ格納用であることを明確にすることができます。
クラス定義を短くかける
前述のサンプルを見ると分かる通り、通常のclassで書くより、
かなり短くクラス定義をかくことができます。
型情報を書くことでデータ構造が見やすくなる。
前述のサンプルコードを見ると分かる通り、
dataclassは、データのフィールドに型情報を簡単に書くことができ、
ひと目で、データ構造を把握することができます。
型情報を書かなくても使うことはできますが、
後述のprintをしたときに、
オブジェクトの中身がうまく表示されないので注意が必要です。
また、dataclassは型情報の検査はしないので、
間違った型を書いても、エラーは出ないので注意が必要です。
Printしたときに、そのままオブジェクトの中身を表示できる。
通常のクラスは、print関数を使っても、オブジェクトのIDが表示されるだけで、
中身を確認するには、replやstr関数をクラスに実装する必要がありますが、
dataclassは、replが自動で作成されるので、
フィールドの型などをちゃんと指定していれば、
printしたときに中身をそのまま確認することが可能です。
class Hoge: def __init__(self, x=0, y=0, name=""): self.x = x self.y = y self.name = name @dataclass class Hoge2: x: int = 0 y: int = 0 name: str = "" @dataclass class Hoge3: x = 0 y = 0 name: str = "" print(Hoge()) # <__main__.Hoge object at 0x104f3ef98> print(Hoge2()) # Hoge2(x=0, y=0, name='') print(Hoge3()) # Hoge3(name='')
asdict関数でdictに変換できる。(Dictから簡単にJSONにも変換できる)
dataclassのオブジェクトは、asdict関数で簡単に辞書型に変換でき、
JSONモジュールを使うことで、簡単にデータをJSON化することもできます。
print(asdict(Hoge2())) # {'x': 0, 'y': 0, 'name': ''} print(astuple(Hoge2())) # (0, 0, '') print(json.dumps(asdict(Hoge2()))) # {"x": 0, "y": 0, "name": ""}
dataclassのネストをしても、ちゃんと変換されます。
@dataclass class Hoge4: hoge: Hoge2 = Hoge2() name: str = "" print(Hoge4()) # Hoge4(hoge=Hoge2(x=0, y=0, name=''), name='') print(asdict(Hoge4())) # {'hoge': {'x': 0, 'y': 0, 'name': ''}, 'name': ''} print(astuple(Hoge4())) # ((0, 0, ''), '') print(json.dumps(asdict(Hoge4()))) # {"hoge": {"x": 0, "y": 0, "name": ""}, "name": ""}
Dict(JSON)からdataclassを作ることもできる
下記のように、同じ形をしたDict (JSON)から、
dataclassを作ることもできます。
@dataclass class Person: name: str age: int nationality: str source_dict = { "name": "Tom", "age": 20, "nationality": "Japan" } person = Person(**source_dict) print(person) # Person(name='Tom', age=20, nationality='Japan')
この方法だと、dictの数が合わなかったり、
dictのkeyが合わないとちゃんとエラーになり、
JSONなどから読み込んだdictをvalidationすることができます。
ただ、残念ながら、keyが間違っている場合はエラーが出ますが、
valueの型が違っても、エラー無しに代入されてしまいますし、
PyCharmの型チェックでも検出されません。
型チェックをしたい場合は、
後述のpost_init関数で自前実装するか、
下記のようなライブラリを使うと、
dictionaryからdataclassに変換する時に、
型チェックをしてくれます。
Frozen引数を使うことで、簡単にイミュータブルにもできる。
変更されたくないデータを含むデータセット(設定値など)では、
通常のクラスでは、@propertyや@getterデコレータを使いますが、
dataclassではfrozen引数を使うことでイミュータブル(変更不可)にできます。
@dataclass(frozen=True) class Hoge5(): hoge: int = 0 name: str = "" hoge5 = Hoge5() hoge5.hoge = 1 # Error: dataclasses.FrozenInstanceError: cannot assign to field 'hoge'
残念ながら、このfrozenはdataclass全体に設定されてしまうので、
個別のフィールドを、ミュータブルにする場合は、
通常のclassと@propertyを使う必要があるようです。
データが作られたときに、自動後処理機能を追加することができる。
dataclassで宣言されたクラスに、post_init() という関数が実装した場合、
その関数は、オブジェクト生成後に自動で実装されます。
これによりデータが作られた時の、自動後処理機能を簡単に実装することができます。
各フィールドの型チェックや、範囲チェックも簡単に実現できます。
例えば、角度を保持するデータクラスがあった時には、
下記のように、post_init関数を使って、自動角度補正なども実現できます。
@dataclass class Hoge7(): angle: int = 0.0 # must be 0 - 2pi def __post_init__(self): self.angle = np.mod(self.angle, 2*np.pi) hoge7 = Hoge7(-3.5 * np.pi) print(hoge7) # Hoge7(angle=1.5707963267948966)
dataclassの残念なところ
コレクションの初期化
公式のドキュメントにも説明が書かれていますが、
list や dict や set をメンバにするときには、
そのデフォルト値を設定するには、下記のように若干冗長にする必要があります。
@dataclass class Path: x: list = field(default_factory=list) y: dict = field(default_factory=dict) z: set = field(default_factory=set) print(Path()) # Path(x=[], y={}, z=set())
自分は、Pythonを書く時はJetbrainsのIDE, PyCharmを使っているので、
下記のようなLive templateを設定して、
できるだけ簡単に入力できるようにしています。
参考資料
Python実践入門 ── 言語の力を引き出し、開発効率を高める (WEB+DB PRESS plusシリーズ)
MyEnigma Supporters
もしこの記事が参考になり、
ブログをサポートしたいと思われた方は、
こちらからよろしくお願いします。