How to overwrite Python Dataclass 'asdict' method
Asked Answered
S

2

15

I have a dataclass, which looks like this:

@dataclass
class myClass:
   id: str
   mode: str
   value: float

This results in:

dataclasses.asdict(myClass)
{"id": id, "mode": mode, "value": value}

But what I want is

{id:{"mode": mode, "value": value}}

I thought I could achive this by adding a to_dict method to my dataclass, which returns the desired dict, but that didn't work.

How could I get my desired result?

Shindig answered 19/10, 2020 at 6:47 Comment(6)
What did you think about the examples of usage in the documentation?Omnipotence
That would leave me with the string "id" instead of the value of myClass.id as the first Key, right? Sorry, I am a total beginner :DShindig
Ok, I see what you mean about "id". So adding a to_dict() method will produce anything you want. In what way did that not work?Omnipotence
adding a "to_dict(self)" method to myClass doesn't change the output of dataclasses.asdict(myClass). If I call the method by myClass.to_dict() it worksShindig
So just call myClass.to_dict() instead.Omnipotence
That was the original plan anyway. The Problem is, that other programmers use my class and using it should be "foolproof", so asdict(myClass) and myClass.to_dict() should return the sameShindig
R
6
from dataclasses import dataclass, asdict


@dataclass
class myClass:
    id: str
    mode: str
    value: float


def my_dict(data):
    return {
        data[0][1]: {
            field: value for field, value in data[1:]
        }
    }


instance = myClass("123", "read", 1.23)

data = {"123": {"mode": "read", "value":  1.23}}

assert asdict(instance, dict_factory=my_dict) == data
Rabbi answered 19/10, 2020 at 10:24 Comment(3)
Is there a overload/override asdict function instead? I need to alter asdict behaviour for some nested dataclasses, but not others.Hogtie
I have a similar requirement. Did you get any way to overload/override asdict function ?Affable
@Hogtie I wrote a new answer for nested dataclasses :)Esparto
E
0

Here is a solution for nested dataclasses.

The relevant underlying implementation is:

# from lib/python3.11/dataclasses.py:1280
def _asdict_inner(obj, dict_factory):
    if _is_dataclass_instance(obj):
        result = []
        for f in fields(obj):
            value = _asdict_inner(getattr(obj, f.name), dict_factory)
            result.append((f.name, value))
        return dict_factory(result)
    # ... (other types below here)

As you can see, the relevant override for dataclasses is fields. Fields calls _FIELDS which refers to self.__dataclass_fields__. Unfortunately this is not easy to overwrite since a lot of other dataclass functions rely on getting the "ground truth" of the base fields from this function.

When an implementation is tightly coupled like this, the easiest fix is just overwriting the library's interface with your desired behavior. Here is how I solved this, making a new override method __dict_factory_override__ (and filtering for dataclass values != 0):

src/classes.py

import math
from dataclasses import dataclass

@dataclass
class TwoLayersDeep:
    f: int = 0

@dataclass
class OneLayerDeep:
    c: TwoLayersDeep
    d: float = 1.0
    e: float = 0.0

    def __dict_factory_override__(self):
        normal_dict = {k: getattr(self, k) for k in self.__dataclass_fields__}
        return {
            k: v for k, v in normal_dict.items()
            if not isinstance(v, float) or not math.isclose(v, 0)
        }

@dataclass
class TopLevelClass:
    a: OneLayerDeep
    b: int = 0

src/dataclasses_override.py

import dataclasses

# If dataclass has __dict_factory_override__, use that instead of dict_factory
_asdict_inner_actual = dataclasses._asdict_inner
def _asdict_inner(obj, dict_factory):
    if dataclasses._is_dataclass_instance(obj):
        if getattr(obj, '__dict_factory_override__', None):
            user_dict = obj.__dict_factory_override__()

            for k, v in user_dict.items(): # in case of further nesting
                if dataclasses._is_dataclass_instance(v):
                    user_dict[k] = _asdict_inner(v, dict_factory)
            return user_dict
    return _asdict_inner_actual(obj, dict_factory)
dataclasses._asdict_inner = _asdict_inner
asdict = dataclasses.asdict

main.py

from src.classes import OneLayerDeep, TopLevelClass, TwoLayersDeep
from src.dataclasses_override import asdict

print(asdict(TopLevelClass(a=OneLayerDeep(c=TwoLayersDeep()))))
# {'a': {'c': {'f': 0}, 'd': 1.0}, 'b': 0}
# As expected, e=0.0 is not printed as it isclose(0)
Esparto answered 7/4, 2024 at 21:21 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.