Python Tips: dataclasses.asdict() の引数 dict_factory の使い方

dataclassses.asdict() の引数 dict_factory の使い方についてかんたんにまとめました。

dataclasses.asdict() とは

dataclasses.asdict() は dataclass を渡すとそれを dict に変換して返してくれる関数です。 フィールドの値が dataclass の場合や、フィールドの値が dict / list / tuple でその中に dataclass が含まれる場合は再帰的に変換を行ってくれます。

from dataclasses import dataclass, asdict
from datetime import date


@dataclass
class Member:
    name: str


@dataclass
class Team:
    name: str
    members: list[Member]
    created_at: date


t1 = Team(
    name='Team A',
    members=[Member(name='Taro'), Member('Hanko')],
    created_at=date(2022, 12, 31),
)
# => Team(name='Team A', members=[Member(name='Taro'), Member(name='Hanko')], created_at=datetime.date(2022, 12, 31))


asdict(t1)
# => {'name': 'Team A', 'members': [{'name': 'Taro'}, {'name': 'Hanko'}], 'created_at': datetime.date(2022, 12, 31)}

引数 dict_factory の使い方

dataclasses.asdict() には dict_factory という非必須の引数があります。 dict 化の処理を差し替えられる機能ですが、記事執筆時点で Python 公式ドキュメントに詳しい説明が載っていません。

dataclasses.asdict() のコードを見るとわかるのですが、 dict_factory には「引数を 1 つ受け取り dict を返す関数」である必要があります。 引数の型は list[tuple[str, typing.Any]] です。 list 内の各 tuple は長さが 2 でフィールドの名前と値を格納しています。

タイプヒント付きの関数を書くと次のようになります。

from typing import Any

def dict_factory(items: list[tuple[str, Any]]) -> dict[str, Any]:
    ...

たとえば、上の Team クラスの場合は次のような関数を渡せば OK です。

def team_dict_factory(items: list[tuple[str, Any]]) -> dict[str, Any]:
    """`Team` 用の `dict_factory`"""
    adict = {}
    for key, value in items:
        if isinstance(value, date):
            value = value.strftime('%Y-%m-%d')
        adict[key] = value

    return adict

d2 = asdict(t1, dict_factory=team_dict_factory)
# => {'name': 'Team A', 'members': [{'name': 'Taro'}, {'name': 'Hanko'}], 'created_at': '2022-12-31'}

参考