Die Standardbibliothek in 3.7 kann eine Datenklasse rekursiv in ein Diktat konvertieren (Beispiel aus den Dokumenten):
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
Ich suche nach einer Möglichkeit, ein Diktat wieder in eine Datenklasse umzuwandeln, wenn es verschachtelt ist. So etwas C(**tmp)
funktioniert nur, wenn die Felder der Datenklasse einfache Typen sind und nicht selbst Datenklassen. Ich bin mit jsonpickle vertraut , das jedoch mit einer auffälligen Sicherheitswarnung versehen ist.
python
python-3.x
python-dataclasses
mbatchkarov
quelle
quelle
elif
,if
die nach verschiedenen Hinweisen sucht . Ich bin mir nicht sicher, wie Sie es auf willkürliche Typhinweise verallgemeinern würden (Dict
undTuple
zusätzlich zumList
Beispiel)asdict
verliert Informationen. Dies wäre im allgemeinen Fall nicht möglich.asdict
keine Informationen darüber gespeichert, aus welcher Klasse das Diktat erstellt wurde. Gegebenclass A: x: int
undclass B: x: int
sollte{'x': 5}
verwendet werden, um eine Instanz vonA
oder zu erstellenB
? Sie gehen anscheinend davon aus, dass die Liste der Attributnamen eine Liste eindeutig definiert und dass eine Zuordnung von Namen zu Datenklassen vorhanden ist, mit denen die richtige Klasse ausgewählt werden kann.Antworten:
Im Folgenden finden Sie die CPython-Implementierung
asdict
- oder speziell die interne rekursive Hilfsfunktion_asdict_inner
, die verwendet wird:# 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
Ruft einfach das Obige mit einigen Behauptungen auf unddict_factory=dict
standardmäßig.1. Typinformationen hinzufügen
Mein Versuch bestand darin, einen benutzerdefinierten Return-Wrapper zu erstellen, der Folgendes erbt
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
Im ursprünglichen Code muss nur die erste Klausel geändert werden, um diesen Wrapper zu verwenden, da die anderen Klauseln nur Container mit
dataclass
-es behandeln:# 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)
Importe:
from dataclasses import dataclass, fields, is_dataclass # thanks to Patrick Haugh from typing import * # deepcopy import copy
Verwendete Funktionen:
# 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 mit den Beispieldatenklassen:
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'>
Ergebnisse sind wie erwartet.
2. Zurückkonvertieren in a
dataclass
Die von verwendete rekursive Routine
asdict
kann mit einigen relativ geringfügigen Änderungen für den umgekehrten Prozess wiederverwendet werden: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)
Verwendete Funktionen:
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)
Prüfung:
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)])
Wieder wie erwartet.
quelle
dataclass
mit vorhandenen Objekttypen gut funktioniert).Ich bin der Autor von
dacite
- dem Tool, das die Erstellung von Datenklassen aus Wörterbüchern vereinfacht.Diese Bibliothek hat nur eine Funktion
from_dict
- dies ist ein kurzes Beispiel für die Verwendung: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)
Darüber hinaus
dacite
unterstützt folgende Funktionen:... und es ist gut getestet - 100% Codeabdeckung!
Verwenden Sie zum Installieren von Dacite einfach pip (oder pipenv):
quelle
Alles was es braucht ist ein Fünfzeiler:
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
Beispielnutzung:
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))
Vollständiger Code, einschließlich von / nach json, hier unter gist: https://gist.github.com/gatopeich/1efd3e1e4269e1e98fae9983bb914f22
quelle
Sie können Mashumaro verwenden, um ein Datenklassenobjekt aus einem Diktat gemäß dem Schema zu erstellen. Mixin aus dieser Bibliothek fügt bequem
from_dict
undto_dict
Methoden zu dataclasses: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
quelle
Wenn Sie JSON von und zu vorhandenen, vordefinierten Datenklassen erstellen möchten, schreiben Sie einfach benutzerdefinierte Encoder- und Decoder-Hooks. Verwenden Sie
dataclasses.asdict()
hier nicht, sondern zeichnen Sie in JSON einen (sicheren) Verweis auf die ursprüngliche Datenklasse auf.jsonpickle
ist nicht sicher, da es Verweise auf beliebige Python-Objekte speichert und Daten an deren Konstruktoren weitergibt. Mit solchen Referenzen kann ich jsonpickle dazu bringen, interne Python-Datenstrukturen zu referenzieren und Funktionen, Klassen und Module nach Belieben zu erstellen und auszuführen. Das heißt aber nicht, dass Sie mit solchen Referenzen nicht unsicher umgehen können. Stellen Sie einfach sicher, dass Sie nur importieren (nicht aufrufen), und stellen Sie dann sicher, dass das Objekt ein tatsächlicher Datenklassentyp ist, bevor Sie es verwenden.Das Framework kann generisch genug gestaltet werden, ist jedoch immer noch nur auf JSON-serialisierbare Typen plus
dataclass
-basierte Instanzen beschränkt :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
Hierbei werden Klassenhinweise im JSON-RPC-Stil verwendet , um die Datenklasse zu benennen. Beim Laden wird überprüft, ob es sich weiterhin um eine Datenklasse mit denselben Feldern handelt. Für die Werte der Felder wird keine Typprüfung durchgeführt (da dies ein ganz anderer Fischkessel ist).
Verwenden Sie diese als
default
undobject_hook
Argumente fürjson.dump[s]()
undjson.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
oder erstellen Sie Instanzen der Klassen
JSONEncoder
undJSONDecoder
mit denselben Hooks.Anstatt vollständig qualifizierende Modul- und Klassennamen zu verwenden, können Sie auch eine separate Registrierung verwenden, um zulässige Typnamen zuzuordnen. Überprüfen Sie die Registrierung beim Codieren und erneut beim Decodieren, um sicherzustellen, dass Sie nicht vergessen, Datenklassen während der Entwicklung zu registrieren.
quelle
Wenn Sie keine zusätzlichen Module verwenden, können Sie die
__post_init__
Funktion verwenden, um diedict
Werte automatisch in den richtigen Typ zu konvertieren . Diese Funktion wird nach aufgerufen__init__
.from dataclasses import dataclass, asdict @dataclass class Bar: fee: str far: str @dataclass class Foo: bar: Bar def __post_init__(self): if isinstance(self.bar, dict): self.bar = Bar(**self.bar) foo = Foo(bar=Bar(fee="La", far="So")) d= asdict(foo) print(d) # {'bar': {'fee': 'La', 'far': 'So'}} o = Foo(**d) print(o) # Foo(bar=Bar(fee='La', far='So'))
Diese Lösung bietet den zusätzlichen Vorteil, dass Objekte ohne Datenklasse verwendet werden können. Solange seine
str
Funktion wieder umgewandelt werden kann, ist es faires Spiel. Zum Beispiel kann es verwendet werden, umstr
Felder wieIP4Address
intern zu halten .quelle
undictify ist eine Bibliothek, die hilfreich sein könnte. Hier ist ein Beispiel für eine minimale Verwendung:
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)
quelle
Validobj macht genau das. Im Vergleich zu anderen Bibliotheken bietet es eine einfachere Oberfläche (derzeit nur eine Funktion) und betont informative Fehlermeldungen. Zum Beispiel bei einem Schema wie
import dataclasses from typing import Optional, List @dataclasses.dataclass class User: name: str phone: Optional[str] = None tasks: List[str] = dataclasses.field(default_factory=list)
Man bekommt einen Fehler wie
>>> import validobj >>> validobj.parse_input({ ... 'phone': '555-1337-000', 'address': 'Somewhereville', 'nme': 'Zahari'}, User ... ) Traceback (most recent call last): ... WrongKeysError: Cannot process value into 'User' because fields do not match. The following required keys are missing: {'name'}. The following keys are unknown: {'nme', 'address'}. Alternatives to invalid value 'nme' include: - name All valid options are: - name - phone - tasks
für einen Tippfehler in einem bestimmten Feld.
quelle
from validated_dc import ValidatedDC from dataclasses import dataclass from typing import List, Union @dataclass class Foo(ValidatedDC): foo: int @dataclass class Bar(ValidatedDC): bar: Union[Foo, List[Foo]] foo = {'foo': 1} instance = Bar(bar=foo) print(instance.get_errors()) # None print(instance) # Bar(bar=Foo(foo=1)) list_foo = [{'foo': 1}, {'foo': 2}] instance = Bar(bar=list_foo) print(instance.get_errors()) # None print(instance) # Bar(bar=[Foo(foo=1), Foo(foo=2)])
validated_dc:
https://github.com/EvgeniyBurdin/validated_dc
Ein detaillierteres Beispiel finden Sie unter:
https://github.com/EvgeniyBurdin/validated_dc/blob/master/examples/detailed.py
quelle
Ich möchte vorschlagen, das zusammengesetzte Muster zu verwenden, um dies zu lösen. Der Hauptvorteil besteht darin, dass Sie diesem Muster weiterhin Klassen hinzufügen können, damit sie sich genauso verhalten.
from dataclasses import dataclass from typing import List @dataclass class CompositeDict: def as_dict(self): retval = dict() for key, value in self.__dict__.items(): if key in self.__dataclass_fields__.keys(): if type(value) is list: retval[key] = [item.as_dict() for item in value] else: retval[key] = value return retval @dataclass class Point(CompositeDict): x: int y: int @dataclass class C(CompositeDict): mylist: List[Point] c = C([Point(0, 0), Point(10, 4)]) tmp = {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]} assert c.as_dict() == tmp
Als Randnotiz können Sie ein Factory-Muster innerhalb der CompositeDict-Klasse verwenden, das andere Fälle wie verschachtelte Dicts, Tupel usw. behandelt, wodurch viel Boilerplate eingespart wird.
quelle