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