Creando objetos de clase de datos nesteds en Python

Tengo un objeto de clase de datos que tiene objetos de clase de datos nesteds. Sin embargo, cuando creo el objeto principal, los objetos nesteds se convierten en un diccionario:

@dataclass class One: f_one: int @dataclass class One: f_one: int f_two: str @dataclass class Two: f_three: str f_four: One data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}} two = Two(**data) 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_2 = Two(**data) two_2 Two(f_three='three', f_four={'f_one': 1, 'f_two': 'two'}) 

Como puede ver, traté de pasar todos los datos como un diccionario, pero no obtuve el resultado deseado. Luego intenté construir el objeto nested primero y pasarlo a través del constructor del objeto, pero obtuve el mismo resultado.

Idealmente me gustaría construir mi objeto para obtener algo como esto:

 Two(f_three='three', f_four=One(f_one=1, f_two='two')) 

¿Hay alguna forma de lograr eso además de convertir manualmente los diccionarios nesteds al objeto de clase de datos correspondiente, siempre que se acceda a los atributos del objeto?

Gracias por adelantado.

Esta es una solicitud que tiene una complejidad que coincide con la complejidad del dataclasses módulo de datos: lo que significa que probablemente la mejor manera de lograr esta capacidad de “campos nesteds” es definir un nuevo decorador, similar a @dataclass .

Afortunadamente, si uno no necesita la firma del método __init__ para reflejar los campos y sus valores predeterminados, como las clases representadas al llamar a la dataclass , esto puede ser mucho más simple: un decorador de clase que llamará a la dataclass original y dataclass poco La funcionalidad sobre su método __init__ generado puede hacerlo con una función de estilo ” ...(*args, **kwargs): “.

En otras palabras, todo lo que hay que hacer es un envoltorio sobre el método __init__ generado que inspeccionará los parámetros pasados ​​en “kwargs”, verifique si alguno corresponde a un “tipo de campo de clase de datos”, y si es así, genere el objeto nested antes de llamando al original __init__ . Tal vez esto sea más difícil de explicar en inglés que en 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 

Tenga en cuenta que además de no preocuparse por la firma __init__ , esto también ignora pasar init=False , ya que de todos modos no tendría sentido.

(El if en la línea de retorno es responsable de que esto funcione, ya sea que se llame con parámetros con nombre o directamente como decorador, como la propia dataclass )

Y en el indicador interactivo:

 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' 

Si desea que se mantenga la firma, recomiendo usar las funciones de ayuda privada en el dataclasses módulo de datos, para crear un nuevo __init__ .

En lugar de escribir un nuevo decorador, se me ocurrió una función que modificaba todos los campos del tipo dataclass una dataclass se inicializaba la clase de dataclass real.

 def dicts_to_dataclasses(instance): """Convert all fields of type `dataclass` into an instance of the specified data class if the current value is of type dict.""" cls = type(instance) for f in dataclasses.fields(cls): if not dataclasses.is_dataclass(f.type): continue value = getattr(instance, f.name) if not isinstance(value, dict): continue new_value = f.type(**value) setattr(instance, f.name, new_value) 

La función podría ser llamada manualmente o en __post_init__ . De esta manera, el decorador @dataclass se puede utilizar en todo su esplendor.

El ejemplo de arriba con una llamada a __post_init__ :

 @dataclass class One: f_one: int f_two: str @dataclass class Two: def __post_init__(self): dicts_to_dataclasses(self) f_three: str f_four: One data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}} two = Two(**data) # Two(f_three='three', f_four=One(f_one=1, f_two='two')) 

Puedes probar el módulo dacite . Este paquete simplifica la creación de clases de datos a partir de diccionarios, también admite estructuras anidadas.

Ejemplo:

 from dataclasses import dataclass from dacite import from_dict @dataclass class A: x: str y: int @dataclass class B: a: A data = { 'a': { 'x': 'test', 'y': 1, } } result = from_dict(data_class=B, data=data) assert result == B(a=A(x='test', y=1)) 

Para instalar dacite, simplemente usa pip:

 $ pip install dacite