Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating nested dataclass objects in Python

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.

like image 822
mohi666 Avatar asked Jul 27 '18 20:07

mohi666


People also ask

What is nested object in Python?

In Python, a nested dictionary is a dictionary inside a dictionary. It's a collection of dictionaries into one single dictionary.

What is a python Dataclass?

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.


1 Answers

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__.

like image 132
jsbueno Avatar answered Sep 20 '22 15:09

jsbueno