MyEnigma

とある自律移動システムエンジニアのブログです。#Robotics #Programing #C++ #Python #MATLAB #Vim #Mathematics #Book #Movie #Traveling #Mac #iPhone

Python3.7で導入されたdataclass入門


Python実践入門 ── 言語の力を引き出し、開発効率を高める (WEB+DB PRESS plusシリーズ)

目次

はじめに

C++ユーザやJuliaユーザがPythonを使っている時の不満の一つとして、

データを格納する目的のstructが無いことが上げられます。

 

もちろんPythonのclassを使って、データのみを格納することもできますが、

記述が冗長になりますし、コールドリーディングしているときに、

データを格納するためのclassなのか、

それともより汎用的なクラスなのかが、一見してわかりにくいことがあります。

 

そこでPython3.7から導入されたのが、dataclassです。

docs.python.org

これを使えば、Pythonでも明示的にデータ格納用のクラスを簡単に実装できます。

また、下記のツイートの通り、アプリケーションの設定データ管理として、

このdataclassを使うと非常に便利です。

 

今回の記事では、この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が表示されるだけで、

中身を確認するには、replstr関数をクラスに実装する必要がありますが、

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に変換する時に、

型チェックをしてくれます。

github.com

 

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を使う必要があるようです。

www.headboost.jp

  

データが作られたときに、自動後処理機能を追加することができる。

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を設定して、

できるだけ簡単に入力できるようにしています。

f:id:meison_amsl:20200307170843p:plain

 

参考資料

docs.python.org

qiita.com

qiita.com

www.reddit.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com


Python実践入門 ── 言語の力を引き出し、開発効率を高める (WEB+DB PRESS plusシリーズ)

MyEnigma Supporters

もしこの記事が参考になり、

ブログをサポートしたいと思われた方は、

こちらからよろしくお願いします。

myenigma.hatenablog.com