La bibliothèque standard de la version 3.7 peut convertir de manière récursive une classe de données en un dict (exemple tiré de la documentation):
from dataclasses import dataclass, asdict
from typing import List
@dataclass
class Point:
x: int
y: int
@dataclass
class C:
mylist: List[Point]
p = Point(10, 20)
assert asdict(p) == {'x': 10, 'y': 20}
c = C([Point(0, 0), Point(10, 4)])
tmp = {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
assert asdict(c) == tmp
Je cherche un moyen de transformer un dict en une classe de données en cas d'imbrication. Quelque chose comme C(**tmp)
ne fonctionne que si les champs de la classe de données sont des types simples et non eux-mêmes des classes de données. Je connais jsonpickle , qui est toutefois accompagné d’un avertissement de sécurité important.
Vous trouverez ci-dessous l'implémentation CPython de asdict
- ou plus précisément, de la fonction d'assistance interne récursive _asdict_inner
qu'elle utilise:
# Source: https://github.com/python/cpython/blob/master/Lib/dataclasses.py
def _asdict_inner(obj, dict_factory):
if _is_dataclass_instance(obj):
result = []
for f in fields(obj):
value = _asdict_inner(getattr(obj, f.name), dict_factory)
result.append((f.name, value))
return dict_factory(result)
Elif isinstance(obj, Tuple) and hasattr(obj, '_fields'):
# [large block of author comments]
return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
Elif isinstance(obj, (list, Tuple)):
# [ditto]
return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
Elif isinstance(obj, dict):
return type(obj)((_asdict_inner(k, dict_factory),
_asdict_inner(v, dict_factory))
for k, v in obj.items())
else:
return copy.deepcopy(obj)
asdict
appelle simplement ce qui précède avec quelques assertions, et dict_factory=dict
par défaut.
Comment cela peut-il être adapté pour créer un dictionnaire de sortie avec le marquage de type requis, comme mentionné dans les commentaires?
1. Ajout d'informations de type
Ma tentative a consisté à créer un wrapper de retour personnalisé héritant de dict
:
class TypeDict(dict):
def __init__(self, t, *args, **kwargs):
super(TypeDict, self).__init__(*args, **kwargs)
if not isinstance(t, type):
raise TypeError("t must be a type")
self._type = t
@property
def type(self):
return self._type
En regardant le code original, seule la première clause doit être modifiée pour utiliser ce wrapper, car les autres clauses ne gèrent que les conteneurs de dataclass
-es:
# only use dict for now; easy to add back later
def _todict_inner(obj):
if is_dataclass_instance(obj):
result = []
for f in fields(obj):
value = _todict_inner(getattr(obj, f.name))
result.append((f.name, value))
return TypeDict(type(obj), result)
Elif isinstance(obj, Tuple) and hasattr(obj, '_fields'):
return type(obj)(*[_todict_inner(v) for v in obj])
Elif isinstance(obj, (list, Tuple)):
return type(obj)(_todict_inner(v) for v in obj)
Elif isinstance(obj, dict):
return type(obj)((_todict_inner(k), _todict_inner(v))
for k, v in obj.items())
else:
return copy.deepcopy(obj)
Importations:
from dataclasses import dataclass, fields, is_dataclass
# thanks to Patrick Haugh
from typing import *
# deepcopy
import copy
Fonctions utilisées:
# copy of the internal function _is_dataclass_instance
def is_dataclass_instance(obj):
return is_dataclass(obj) and not is_dataclass(obj.type)
# the adapted version of asdict
def todict(obj):
if not is_dataclass_instance(obj):
raise TypeError("todict() should be called on dataclass instances")
return _todict_inner(obj)
Tests avec les exemples de classes de données:
c = C([Point(0, 0), Point(10, 4)])
print(c)
cd = todict(c)
print(cd)
# {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
print(cd.type)
# <class '__main__.C'>
Les résultats sont comme prévu.
2. Reconvertir en dataclass
La routine récursive utilisée par asdict
peut être réutilisée pour le processus inverse, avec quelques modifications relativement mineures:
def _fromdict_inner(obj):
# reconstruct the dataclass using the type tag
if is_dataclass_dict(obj):
result = {}
for name, data in obj.items():
result[name] = _fromdict_inner(data)
return obj.type(**result)
# exactly the same as before (without the Tuple clause)
Elif isinstance(obj, (list, Tuple)):
return type(obj)(_fromdict_inner(v) for v in obj)
Elif isinstance(obj, dict):
return type(obj)((_fromdict_inner(k), _fromdict_inner(v))
for k, v in obj.items())
else:
return copy.deepcopy(obj)
Fonctions utilisées:
def is_dataclass_dict(obj):
return isinstance(obj, TypeDict)
def fromdict(obj):
if not is_dataclass_dict(obj):
raise TypeError("fromdict() should be called on TypeDict instances")
return _fromdict_inner(obj)
Tester:
c = C([Point(0, 0), Point(10, 4)])
cd = todict(c)
cf = fromdict(cd)
print(c)
# C(mylist=[Point(x=0, y=0), Point(x=10, y=4)])
print(cf)
# C(mylist=[Point(x=0, y=0), Point(x=10, y=4)])
Encore une fois comme prévu.
Je suis l'auteur de dacite
- l'outil qui simplifie la création de classes de données à partir de dictionnaires.
Cette bibliothèque n'a qu'une fonction from_dict
- voici un exemple d'utilisation rapide:
from dataclasses import dataclass
from dacite import from_dict
@dataclass
class User:
name: str
age: int
is_active: bool
data = {
'name': 'john',
'age': 30,
'is_active': True,
}
user = from_dict(data_class=User, data=data)
assert user == User(name='john', age=30, is_active=True)
De plus, dacite
prend en charge les fonctionnalités suivantes:
... et c'est bien testé - couverture de code à 100%!
Pour installer dacite, utilisez simplement pip (ou pipenv):
$ pip install dacite
Vous pouvez utiliser mashumaro pour créer un objet dataclass à partir d'un dict selon le schéma. Mixin de cette bibliothèque ajoute des méthodes pratiques from_dict
et to_dict
à des classes de données:
from dataclasses import dataclass
from typing import List
from mashumaro import DataClassDictMixin
@dataclass
class Point(DataClassDictMixin):
x: int
y: int
@dataclass
class C(DataClassDictMixin):
mylist: List[Point]
p = Point(10, 20)
tmp = {'x': 10, 'y': 20}
assert p.to_dict() == tmp
assert Point.from_dict(tmp) == p
c = C([Point(0, 0), Point(10, 4)])
tmp = {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
assert c.to_dict() == tmp
assert C.from_dict(tmp) == c
Tout ce qu'il faut, c'est un cinq lignes:
def dataclass_from_dict(klass, d):
try:
fieldtypes = {f.name:f.type for f in dataclasses.fields(klass)}
return klass(**{f:dataclass_from_dict(fieldtypes[f],d[f]) for f in d})
except:
return d # Not a dataclass field
Exemple d'utilisation:
from dataclasses import dataclass, asdict
@dataclass
class Point:
x: float
y: float
@dataclass
class Line:
a: Point
b: Point
line = Line(Point(1,2), Point(3,4))
assert line == dataclass_from_dict(Line, asdict(line))
Code complet, y compris vers/depuis json, ici chez Gist: https://Gist.github.com/gatopeich/1efd3e1e4269e1e98fae9983bb914f22
Si votre objectif est de produire JSON à partir de et vers des classes de données existantes prédéfinies , il suffit d'écrire des crochets de codeur et de décodeur personnalisés . N'utilisez pas dataclasses.asdict()
ici, mais enregistrez en JSON une référence (sécurisée) à la classe de données d'origine.
jsonpickle
n'est pas sûr car il stocke les références à des objets arbitraires Python et transmet les données à leurs constructeurs. Avec de telles références, jsonpickle peut faire référence à des structures de données internes Python et créer et exécuter des fonctions, des classes et des modules à volonté. Mais cela ne signifie pas que vous ne pouvez pas gérer de telles références de manière non sécurisée. Vérifiez simplement que vous importez (et non pas appelez), puis vérifiez que l'objet est un type de classe de données réel avant de l'utiliser.
La structure peut être rendue suffisamment générique, mais néanmoins limitée aux types JSON-serialisable plus instances dataclass
-:
import dataclasses
import importlib
import sys
def dataclass_object_dump(ob):
datacls = type(ob)
if not dataclasses.is_dataclass(datacls):
raise TypeError(f"Expected dataclass instance, got '{datacls!r}' object")
mod = sys.modules.get(datacls.__module__)
if mod is None or not hasattr(mod, datacls.__qualname__):
raise ValueError(f"Can't resolve '{datacls!r}' reference")
ref = f"{datacls.__module__}.{datacls.__qualname__}"
fields = (f.name for f in dataclasses.fields(ob))
return {**{f: getattr(ob, f) for f in fields}, '__dataclass__': ref}
def dataclass_object_load(d):
ref = d.pop('__dataclass__', None)
if ref is None:
return d
try:
modname, hasdot, qualname = ref.rpartition('.')
module = importlib.import_module(modname)
datacls = getattr(module, qualname)
if not dataclasses.is_dataclass(datacls) or not isinstance(datacls, type):
raise ValueError
return datacls(**d)
except (ModuleNotFoundError, ValueError, AttributeError, TypeError):
raise ValueError(f"Invalid dataclass reference {ref!r}") from None
Ceci utilise indications de classe de style JSON-RPC pour nommer la classe de données. Lors du chargement, il est vérifié que cette classe de données contient toujours les mêmes champs. Aucune vérification de type n'est effectuée sur les valeurs des champs (car il s'agit d'une marmite de poissons totalement différente).
Utilisez-les comme arguments default
et object_hook
pour json.dump[s]()
et json.dump[s]()
:
>>> print(json.dumps(c, default=dataclass_object_dump, indent=4))
{
"mylist": [
{
"x": 0,
"y": 0,
"__dataclass__": "__main__.Point"
},
{
"x": 10,
"y": 4,
"__dataclass__": "__main__.Point"
}
],
"__dataclass__": "__main__.C"
}
>>> json.loads(json.dumps(c, default=dataclass_object_dump), object_hook=dataclass_object_load)
C(mylist=[Point(x=0, y=0), Point(x=10, y=4)])
>>> json.loads(json.dumps(c, default=dataclass_object_dump), object_hook=dataclass_object_load) == c
True
ou créez des instances des classes JSONEncoder
et JSONDecoder
avec les mêmes points d'ancrage.
Au lieu d'utiliser des noms de module et de classe entièrement éligibles, vous pouvez également utiliser un registre distinct pour mapper les noms de types autorisés; vérifiez le registre sur l'encodage, et encore sur le décodage pour vous assurer de ne pas oublier d'enregistrer des classes de données au fur et à mesure que vous développez.
ndictify est une bibliothèque qui pourrait être utile. Voici un exemple d'utilisation minimale:
import json
from dataclasses import dataclass
from typing import List, NamedTuple, Optional, Any
from undictify import type_checked_constructor
@type_checked_constructor(skip=True)
@dataclass
class Heart:
weight_in_kg: float
Pulse_at_rest: int
@type_checked_constructor(skip=True)
@dataclass
class Human:
id: int
name: str
nick: Optional[str]
heart: Heart
friend_ids: List[int]
tobias_dict = json.loads('''
{
"id": 1,
"name": "Tobias",
"heart": {
"weight_in_kg": 0.31,
"Pulse_at_rest": 52
},
"friend_ids": [2, 3, 4, 5]
}''')
tobias = Human(**tobias_dict)