2022年12月21日水曜日

dataclass を使って配列内にある辞書データをオブジェクトに変換する方法

dataclass を使って配列内にある辞書データをオブジェクトに変換する方法

概要

前回 dataclass を使った JSON <-> オブジェクトの変換方法を紹介しました
今回はその中でもクセのある辞書データが配列内に直接置かれている場合を紹介したいと思います

また配列内にある辞書データはデータ構造がデータごとに異なる場合を想定しています

環境

  • macOS 11.7.1
  • Python 3.10.2
  • dataclass-wizard 0.22.2

サンプルコード

from dataclasses import dataclass
from dataclass_wizard import JSONWizard
from typing import (List,
                    Optional)


@dataclass
class Framework():
    sinatra: str
    flask: str


@dataclass
class Lang():
    ruby: str
    python: str


@dataclass
class Profile():
    lang: Optional[List[Lang]] = None
    framework: Optional[List[Framework]] = None


@dataclass
class UserData(JSONWizard):
    name: str
    profile: List[Profile]


if __name__ == "__main__":
    data = {
        "name": "hawksnowlog",
        "profile": [
            {
                "lang": [
                    {
                        "ruby": "3.1.2",
                        "python": "3.10.2",
                    }
                ]
            },
            {
                "framework": [
                    {
                        "sinatra": "3.0.0",
                        "flask": "1.0.0",
                    }
                ]
            },
        ]
    }
    result = UserData.from_dict(data)
    print(result)
    print(result.name)
    print(result.profile[0].lang[0].ruby)
    print(result.profile[1].framework[0].sinatra)

ちょっと解説

今回は profile というフィールドが配列でこの中にデータ構造の異なる辞書データが複数個入ることを想定しています

ポイントは Profile クラスでここにデータ構造が異なるデータをデフォルト None ですべて定義する点です

配列の場合順番にオブジェクトに変換されます 最初は lang 情報が変換されます
Profile クラスに渡されるデータは lang 情報だけなので必ず他のフィールドはデフォルト値で初期化しておく必要があります
lang 変換時には framework フィールドの情報は渡されないので None が設定されます

つまり配列内の最初のデータは lang がオブジェクトに変換され framework が None として変換されます

配列の2つ目の値は framework になります
framework が Profile クラスに渡されて変換されます
同様に framework 情報のみが渡されるので lang は None として変換されます

このように配列それぞれのデータごとに変換が行われるので配列内で管理するデータ構造分定義する必要がありまたそれぞれのフィールドはデフォルト値を必ず設定する必要があります

tuple でもいけます

あとから気づいたんですが tuple を使ってもいけます
この場合は tuple に定義する順番は配列内の順番と同じである必要があります
None もないのでこちらのほうが綺麗かもしれません

from dataclasses import dataclass
from dataclass_wizard import JSONWizard
from typing import List


@dataclass
class Framework():
    sinatra: str
    flask: str


@dataclass
class Lang():
    ruby: str
    python: str


@dataclass
class Frameworks():
    framework: List[Framework]


@dataclass
class Langs():
    lang: List[Lang]


@dataclass
class UserData(JSONWizard):
    name: str
    profile: tuple[Langs, Frameworks]


if __name__ == "__main__":
    data = {
        "name": "hawksnowlog",
        "profile": [
            {
                "lang": [
                    {
                        "ruby": "3.1.2",
                        "python": "3.10.2",
                    }
                ]
            },
            {
                "framework": [
                    {
                        "sinatra": "3.0.0",
                        "flask": "1.0.0",
                    }
                ]
            },
        ]
    }
    result = UserData.from_dict(data)
    print(result)
    print(result.name)
    print(result.profile[0].lang[0].ruby)
    print(result.profile[1].framework[0].sinatra)

更に配列の順番が固定であればオブジェクトから直接参照できるようにできる

例えば今回のように 1 番目が lang 2 番目が framework のように配列に格納されている順番が保証されているのであればわざわざ profile からインデックスで参照せずに直接 lang と framework を参照してもいいと思います

その場合は __post_init__ と組み合わせるといい感じになると思います

from dataclasses import dataclass
from dataclass_wizard import JSONWizard
from typing import (List,
                    Optional)


@dataclass
class Framework():
    sinatra: str
    flask: str


@dataclass
class Lang():
    ruby: str
    python: str


@dataclass
class Frameworks():
    framework: List[Framework]


@dataclass
class Langs():
    lang: List[Lang]


@dataclass
class Profile():
    lang: List[Lang]
    framework: List[Framework]


@dataclass
class UserData(JSONWizard):
    name: str
    profile: tuple[Langs, Frameworks]
    p: Optional[Profile] = None

    def __post_init__(self):
        self.p = Profile(self.profile[0].lang,
                         self.profile[1].framework)


if __name__ == "__main__":
    data = {
        "name": "hawksnowlog",
        "profile": [
            {
                "lang": [
                    {
                        "ruby": "3.1.2",
                        "python": "3.10.2",
                    }
                ]
            },
            {
                "framework": [
                    {
                        "sinatra": "3.0.0",
                        "flask": "1.0.0",
                    }
                ]
            },
        ]
    }
    result = UserData.from_dict(data)
    print(result)
    print(result.name)
    print(result.profile[0].lang[0].ruby)
    print(result.profile[1].framework[0].sinatra)
    print(result.p.lang[0].ruby)
    print(result.p.framework[0].sinatra)

最後に

オブジェクトに変換する場合は JSON の構造に合わせて型を定義する必要があるのでうまく行かない場合は型定義がちゃんと JSON から抽出できているかを確認しましょう

参考サイト

0 件のコメント:

コメントを投稿