I have a dataclass object that has nested dataclass objects in it. However, when I create the main object, the nested objects turn into a dictionary:
@dataclass class One: f_one: int f_two: str @dataclass class Two: f_three: str f_four: One Two(**{'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}) Two(f_three='three', f_four={'f_one': 1, 'f_two': 'two'}) obj = {'f_three': 'three', 'f_four': One(**{'f_one': 1, 'f_two': 'two'})} Two(**obj) Two(f_three='three', f_four=One(f_one=1, f_two='two'))
As you can see only **obj
works.
Ideally I'd like to construct my object to get something like this:
Two(f_three='three', f_four=One(f_one=1, f_two='two'))
Is there any way to achieve that other than manually converting nested dictionaries to corresponding dataclass object, whenever accessing object attributes?
Thanks in advance.
In Python, a nested dictionary is a dictionary inside a dictionary. It's a collection of dictionaries into one single dictionary.
Python introduced the dataclass in version 3.7 (PEP 557). The dataclass allows you to define classes with less code and more functionality out of the box. The following defines a regular Person class with two instance attributes name and age : class Person: def __init__(self, name, age): self.name = name self.age = age.
This is a request that is as complex as the dataclasses
module itself, which means that probably the best way to achieve this "nested fields" capability is to define a new decorator, akin to @dataclass
.
Fortunately, if you don't need the signature of the __init__
method to reflect the fields and their defaults, like the classes rendered by calling dataclass
, this can be a whole lot simpler: A class decorator that will call the original dataclass
and wrap some functionality over its generated __init__
method can do it with a plain "...(*args, **kwargs):
" style function.
In other words, all one needs to do is write a wrapper around the generated __init__
method that will inspect the parameters passed in "kwargs", check if any corresponds to a "dataclass field type", and if so, generate the nested object prior to calling the original __init__
. Maybe this is harder to spell out in English than in Python:
from dataclasses import dataclass, is_dataclass def nested_dataclass(*args, **kwargs): def wrapper(cls): cls = dataclass(cls, **kwargs) original_init = cls.__init__ def __init__(self, *args, **kwargs): for name, value in kwargs.items(): field_type = cls.__annotations__.get(name, None) if is_dataclass(field_type) and isinstance(value, dict): new_obj = field_type(**value) kwargs[name] = new_obj original_init(self, *args, **kwargs) cls.__init__ = __init__ return cls return wrapper(args[0]) if args else wrapper
Note that besides not worrying about __init__
signature, this also ignores passing init=False
- since it would be meaningless anyway.
(The if
in the return line is responsible for this to work either being called with named parameters or directly as a decorator, like dataclass
itself)
And on the interactive prompt:
In [85]: @dataclass ...: class A: ...: b: int = 0 ...: c: str = "" ...: In [86]: @dataclass ...: class A: ...: one: int = 0 ...: two: str = "" ...: ...: In [87]: @nested_dataclass ...: class B: ...: three: A ...: four: str ...: In [88]: @nested_dataclass ...: class C: ...: five: B ...: six: str ...: ...: In [89]: obj = C(five={"three":{"one": 23, "two":"narf"}, "four": "zort"}, six="fnord") In [90]: obj.five.three.two Out[90]: 'narf'
If you want the signature to be kept, I'd recommend using the private helper functions in the dataclasses
module itself, to create a new __init__
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With