J'ai un objet de classe de données qui contient des objets de classe de données imbriqués. Cependant, lorsque je crée l'objet principal, les objets imbriqués se transforment en dictionnaire:
@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'})
Comme vous pouvez le voir, j'ai essayé de transmettre toutes les données sous forme de dictionnaire, mais je n'ai pas obtenu le résultat souhaité. Ensuite, j'ai essayé de construire l'objet imbriqué en premier et de le passer par le constructeur d'objet, mais j'ai obtenu le même résultat.
Idéalement, j'aimerais construire mon objet pour obtenir quelque chose comme ceci:
Two(f_three='three', f_four=One(f_one=1, f_two='two'))
Existe-t-il un moyen d'y parvenir autre que la conversion manuelle des dictionnaires imbriqués en objet de classe de données correspondant, chaque fois que vous accédez à des attributs d'objet?
Merci d'avance.
Il s'agit d'une requête dont la complexité correspond à la complexité du module dataclasses
lui-même: ce qui signifie que probablement la meilleure façon d'atteindre cette capacité de "champs imbriqués" est de définir un nouveau décorateur, semblable à @dataclass
.
Heureusement, si l'on n'a pas besoin de la signature de la méthode __init__
Pour refléter les champs et leurs valeurs par défaut, comme les classes rendues en appelant dataclass
, cela peut être beaucoup plus simple: une classe Le décorateur qui appellera le dataclass
d'origine et encapsulera certaines fonctionnalités sur sa méthode __init__
générée peut le faire avec une simple fonction de style "...(*args, **kwargs):
".
En d'autres termes, tout ce qu'il faut faire est un wrapper sur la méthode générée __init__
Qui inspectera les paramètres passés dans "kwargs", vérifier si l'un correspond à un "type de champ de classe de données", et si oui, générer l'objet imbriqué avant d'appeler l'original __init__
. C'est peut-être plus difficile à préciser en anglais qu'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
Notez qu'en plus de ne pas vous soucier de la signature de __init__
, Cela ignore également le passage de init=False
- car cela n'aurait aucun sens de toute façon.
(Le if
dans la ligne de retour est responsable pour que cela fonctionne soit en étant appelé avec des paramètres nommés soit directement en tant que décorateur, comme dataclass
lui-même)
Et sur l'invite interactive:
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 vous voulez que la signature soit conservée, je vous recommande d'utiliser les fonctions d'assistance privées dans le module dataclasses
lui-même, pour créer un nouveau __init__
.
Vous pouvez essayer le module dacite
. Ce package simplifie la création de classes de données à partir de dictionnaires - il prend également en charge les structures imbriquées.
Exemple:
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))
Pour installer dacite, utilisez simplement pip:
$ pip install dacite
Au lieu d'écrire un nouveau décorateur, j'ai proposé une fonction modifiant tous les champs de type dataclass
après l'initialisation du dataclass
réel.
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 fonction peut être appelée manuellement ou en __post_init__
. De cette façon, le @dataclass
le décorateur peut être utilisé dans toute sa splendeur.
L'exemple ci-dessus avec un appel à __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'))
J'ai créé une extension de la solution par @jsbueno qui accepte également la saisie sous la forme List[<your class/>]
.
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 isinstance(value, list):
if field_type.__Origin__ == list or field_type.__Origin__ == List:
sub_type = field_type.__args__[0]
if is_dataclass(sub_type):
items = []
for child in value:
if isinstance(child, dict):
items.append(sub_type(**child))
kwargs[name] = items
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