python

【Python】dataclassでネストされたdictを一発でdataclass化する方法をちゃちゃっと解説

今回はdataclassでネスト化されたdictを一回でdataclass化する方法を解説します。

dataclassについてはこちらの記事が参考になります。

https://zenn.dev/enven/articles/8b80ff38461b4ff329aa

[詳解] Pythonのdataclasses

https://docs.python.org/ja/3/library/dataclasses.html

dataclasses — データクラス

pydanticを使うべし

結論から申し上げると

pydanticライブラリのdataclasses.dataclassを使用する

になります。

詳しい使い方は下記に記します。

通常のdataclassはネストされたdictを一発変換できない

dataclassは引数に**dict型を渡すことで一気にクラス化することができます。

from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int
    note: str


# 定義したフィールドとkeyが一致すれば良い
user_info = {
    "name": "taro suzuki",
    "age": 21,
    "note": "sample",
}


# dictデータを一気にdataclass化
# User(name='taro suzuki', age=21, note='sample')
user = User(**user_info)

# この時点でネストされていない
print(user)

# 23
print(user.age)

# taro suzuki
print(user.name)

上記サンプルコードのようにクラス名(**dictデータ)とすることで一発でクラス化することができます。

ただし、このやり方はdictデータがネストされていない状態に限ります。

ネストされているとどうなるか

ネストされている場合、ネスト部分はdictのまま保管されてしまいます。

以下のコードを見てください。

from dataclasses import dataclass


@dataclass
class Name:
    first_name: str
    last_name: str


@dataclass
class User:
    name: Name
    age: int
    note: str


user_info = {
    "name": {
        "first_name": "taro",
        "last_name": "suzuki",
    },
    "age": 21,
    "note": "sample",
}


user = User(**user_info)

# User(name={'first_name': 'taro', 'last_name': 'suzuki'}, age=21, note='sample')
# この時点でネストされていない
print(user)

# 23
print(user.age)

# 例外発生
# print(user.name.first_name)

先程のコードから変更を加えました。

  • Nameクラスを新たに定義
  • UserクラスのnameフィールドにNameクラスを紐づけ

この状態でdictデータを準備して、先程と同じようにクラス化します。

そうすると以下の状態になります。

  • Userクラスのage, noteフィールドは問題ない
  • nameフィールドはNameクラスに変換されず、dict型として残る

期待値であるNameクラスへの変換を行ってくれません。

そのため、user.name.first_nameのように要素を指定すると例外発生となります。

また、型ヒントはあくまでヒントであるため、int型にstr型をいれても処理は通ります。

from dataclasses import dataclass


@dataclass
class Name:
    first_name: str
    last_name: str


@dataclass
class User:
    name: Name
    age: int
    note: str


user_info = {
    "name": {
        "first_name": "taro",
        "last_name": "suzuki",
    },
    "age": "aaa",  # str型を定義
    "note": "sample",
}


# インススタンス化できてしまう。
user = User(**user_info)

# User(name={'first_name': 'taro', 'last_name': 'suzuki'}, age='aaa', note='sample')
print(user)

# aaa
print(user.age)

上記コードでは本来int型となるageフィールドにstr型を代入しています。

この場合、特段例外になるわけではなく処理は通ってしまいます。

型ヒントとはあくまでコーティング中の助けという意味合いが強いのでしょうね。

一応解決策はある

ネストされたdictを型変換できないのか

一応解決策はあります。

それがこちら

from dataclasses import dataclass


@dataclass
class Name:
    first_name: str
    last_name: str


@dataclass
class User:
    name: Name
    age: int
    note: str

    def __post_init__(self):
        # もう一度dict展開を行う
        self.name = Name(**self.name)


user_info = {
    "name": {
        "first_name": "taro",
        "last_name": "suzuki",
    },
    "age": 21,
    "note": "sample",
}


user = User(**user_info)

# User(name={'first_name': 'taro', 'last_name': 'suzuki'}, age=21, note='sample')
# この時点でネストされていない
print(user)

# 23
print(user.age)

# taro
print(user.name.first_name)

注目するのはUserクラスです。

@dataclass
class User:
    name: Name
    age: int
    note: str

    def __post_init__(self):
        # もう一度dict展開を行う
        self.name = Name(**self.name)

__post_init__()で最初期化を行うことでネストされたdictのdataclass化を行います。

ただ、この方法だとpylanceに怒られるので推奨されるやり方ではないです。

pydantic.dataclassはネストも一発で解消する

というわけで、ネストされたdictを一発で変換する方法になります。

ついでに型もちゃんと見てほしい…。

この問題を解決するのはpydanticライブラリになります。

pydanticは型ヒントを活用して厳密な型検証を行ってくれます。

FastAPIを使ったことある人なら馴染み深いやつです。

公式ドキュメントが読みやすいので個人的に好きです。

https://pydantic-docs.helpmanual.io/

このpydanticにはdataclassによる型検証を行ってくれる機能があります。

これを利用してネストしたdictを強制的にdataclass化してくれるわけです。

実際に使ってみましょう。

pydanticでdataclassを使う

pydanticは外部ライブラリなのでpip installをします。

pip install pydantic

サンプルコードは以下になります。

from pydantic.dataclasses import dataclass


@dataclass
class Name:
    first_name: str
    last_name: str


@dataclass
class User:
    name: Name
    age: int


user_info = {
    "name": {
        "first_name": "taro",
        "last_name": "suzuki",
    },
    "age": 23,
    "note": "sample",
}


user = User(**user_info)

# User(name=Name(first_name='taro', last_name='suzuki'), age=23)
print(user)

# 23
print(user.age)

# taro
print(user.name.first_name)

先程のコードと変わったのは一番上の行だけです。

pydanticを用いたdataclassを定義するにはfrom pydantic.dataclasses import dataclassを書くだけで良いです。

このコードであればネストされたdictもクラス化してくれます。

クラス化されているため、user.name.first_nameで要素名を出力することができます。

pydantic.dataclassであれば型検証もやってくれる

また、pydanticであるため型検証も行ってくれます。

先程、ageフィールドにstr型をいれた場合、例外にならず処理が完了しました。

pydantic.dataclassだとどうでしょうか。

from pydantic.dataclasses import dataclass


@dataclass
class Name:
    first_name: str
    last_name: str


@dataclass
class User:
    name: Name
    age: int


user_info = {
    "name": {
        "first_name": "taro",
        "last_name": "suzuki",
    },
    "age": "aaa",
    "note": "sample",
}


user = User(**user_info)

# Traceback (most recent call last):
# ...
# pydantic.error_wrappers.ValidationError: 1 validation error for User
# age
#   value is not a valid integer (type=type_error.integer)

pydanticは型変換を試みて、変換できなかった場合にpydantic.error_wrappers.ValidationErrorという例外を発生させます。

これであれば、型ヒントの恩恵を最大限に受けながら開発もできてしまうというわけです。

まとめ

今回はdataclassでネスト化されたdictを一回でdataclass化する方法を解説しました。

まとめです。

  • ネストされたdict型のdataclass化にはpydantic.dataclassを使用する。
  • pydanticを使用するためにはpip install pydantic
  • pydantic.dataclassはネストされたdictの変換を行いつつ、型検証まで行ってくれる。

以上になります。