yoraba build

備忘録を兼ねた技術ブログ

dataclassのJSONパーサー

pythonでデータを扱う際に便利なdataclassを、
JSONシリアライズすることにより、
構成ファイルを作成します。

dataclassの仕様

 データの集合を定義するクラスです。dataclassデコレータで修飾することで利用できます。
 クラス変数がデータメンバとして扱われます。クラス変数を初期化するためのコンストラクタが自動的に作成されます。
 クラス変数にデフォルト値を設定することができ、コンストラクタではキーワード引数により初期化出来ます。dataclassデコレータで並び順の指定を行わなかった場合、クラス変数の定義順と、コンストラクタにおける仮引数の順番が同一になります。キーワード引数は位置引数よりも後に定義しなければならないという規則が適用されるので、クラス変数の定義順には注意が必要です。
 asdict関数でdataclassを辞書に変換することが出来ます。dataclassのコンストラクタに辞書をアンパックして渡すことで初期化が可能なので、辞書との相互変換が可能です。

from dataclasses import dataclass

@dataclass
class ConfigData:
    config1: str
    config2: str = "value2"
    config3: int = 3

dataclassの利点

  • 変数が入力候補に表示される

 configparserなどで用いるデータはアクセスしたいデータのキーを文字列で入力しなければなりません。
 dataclassを用いれば、IDEのインテリセンスで入力候補が表示されるので、タイプミスのリスクを低減出来ます。

  • コンストラクタを省略できる

 データしか定義しないクラスで、コンストラクタをいちいち定義するのは冗長です。
 dataclassを用いることで、コンストラクタが自動生成されるのでコード量を減らすことが出来ます。
 また、コンストラクタがatomic mutatorであることが保証されます。つまり、仮引数の定義忘れによる変数未初期化という障害を回避できます。

dataclassのJSONエンコーダー

class Dataclass2JSONEncoder(json.JSONEncoder):
    def default(self, o):
        if dataclasses.is_dataclass(o):
            return dataclasses.asdict(o)
        return super().default(o)

 dataclassは辞書と相互変換できるので、JSONにおいても辞書データとして扱います。
 JSONEncoderクラスを継承させたクラスを作成し、default関数のオーバーライド内で、オブジェクトがdataclassかどうか判定し、真だった場合は辞書に変換して返しています。

Dataclass構成ファイルシリアライザ

class DataclassConfigSerializer:
    def __init__(self, path='./config.json'):
        self._path = path

    def load_config(self, data_type):
        return data_type(** self.load_config_asdict())

    def load_config_asdict(self):
        with open(self._path, mode='r', encoding='utf-8') as f:
            dict_data = json.load(f)
        return dict_data

    def save_config(self, data):
        with open(self._path, mode='w', encoding='utf-8') as f:
            json.dump(data, f, cls=Dataclass2JSONEncoder, indent=4)

構成ファイルの配置pathを指定します。

  • save_config関数

dataclassを渡しシリアライズします。
dump関数に前の項目で作成したエンコーダを指定しています。

  • load_config関数

dataclass構成ファイルの型を指定します。
load_config_asdict関数で構成ファイルを辞書として読み込みます。
引数として渡された型に()をつけることでコンストラクタにアクセス出来ます。引数として辞書のアンパックを渡します。

利用例

def test_save_config(self):
    serializer = DataclassConfigSerializer()
    serializer.save_config(ConfigData('1', '2', 3))

def test_load_config(self):
    serializer = DataclassConfigSerializer()
    data: ConfigData = serializer.load_config(ConfigData)