Python-Datenklasse aus einem verschachtelten Diktat

72

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.

mbatchkarov
quelle
Die Frage, von der dies als Duplikat markiert ist, stellt zwar dasselbe, aber die dort gegebene Antwort funktioniert für dieses spezielle Beispiel nicht. Ich habe dort einen Kommentar hinterlassen und suche immer noch nach einer allgemeineren Antwort.
mbatchkarov
Könnten Sie diesen Unterschied hier explizit machen? Es sieht so aus, als müssten Sie möglicherweise eine hinzufügen elif, ifdie nach verschiedenen Hinweisen sucht . Ich bin mir nicht sicher, wie Sie es auf willkürliche Typhinweise verallgemeinern würden ( Dictund Tuplezusätzlich zum ListBeispiel)
Patrick Haugh
5
asdictverliert Informationen. Dies wäre im allgemeinen Fall nicht möglich.
wim
6
Insbesondere werden asdictkeine Informationen darüber gespeichert, aus welcher Klasse das Diktat erstellt wurde. Gegeben class A: x: intund class B: x: intsollte {'x': 5}verwendet werden, um eine Instanz von Aoder zu erstellen B? 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.
Chepper
2
Ich würde Ihnen empfehlen, diese Bibliothek zu besuchen .
Abdul Niyas PM

Antworten:

37

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)

asdictRuft einfach das Obige mit einigen Behauptungen auf und dict_factory=dictstandardmäßig.

Wie kann dies angepasst werden, um ein Ausgabewörterbuch mit der erforderlichen Typkennzeichnung zu erstellen, wie in den Kommentaren erwähnt?


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 asdictkann 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.

meowgoesthedog
quelle
9
TL; DR, +1 für die Vollständigkeit der Antwort.
iBug
3
+0: ​​+1 für den Versuch, aber -1, weil es im Grunde eine schlechte Idee ist.
wim
1
@wim Ich würde tbh zustimmen - kann es nicht viel mehr als eine theoretische Übung sehen (die zumindest zeigt, dass sie dataclassmit vorhandenen Objekttypen gut funktioniert).
Meowgoesthedog
Ich werde dies akzeptieren, da es die umfassendste Antwort ist, die zukünftigen Benutzern hilft, den Kern des Problems zu verstehen. Am Ende hatte ich etwas, das @ Martijns Vorschlag näher kam, da ich tatsächlich JSON wollte. Vielen Dank an alle für Ihre Antworten
mbatchkarov
64

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 daciteunterstützt folgende Funktionen:

  • verschachtelte Strukturen
  • (Grund-) Typenprüfung
  • optionale Felder (dh Eingabe.Optional)
  • Gewerkschaften
  • Sammlungen
  • Werte Casting und Transformation
  • Neuzuordnung von Feldnamen

... und es ist gut getestet - 100% Codeabdeckung!

Verwenden Sie zum Installieren von Dacite einfach pip (oder pipenv):

$ pip install dacite
Konrad Hałas
quelle
2
genial! Wie können wir vorschlagen, diese Funktionalität zur Python-Standardbibliothek hinzuzufügen? :-)
Alex
netter @Konrad :)
Gideon
das ist toll! Ich würde gerne sehen, wie dies so schnell wie möglich zu nativen Python-Datenklassen hinzugefügt wird
Esostack
1
Ich kann nicht verstehen, warum zum Teufel Python Datenklassen bringt, aber keine Möglichkeit hinzugefügt, sie aus einem Wörterbuch einschließlich verschachtelter Klassen zu erstellen.
Jurass
20

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

gatopeich
quelle
9

Sie können Mashumaro verwenden, um ein Datenklassenobjekt aus einem Diktat gemäß dem Schema zu erstellen. Mixin aus dieser Bibliothek fügt bequem from_dictund to_dictMethoden 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
tikhonov_a
quelle
1
Wow, das ist großartig. Wenn die Abhängigkeiten von msgpack und pyyaml ​​optional wären, könnte ich sehen, dass dies irgendwann in der Standardbibliothek enthalten ist. Das Hinzufügen von Serialisierung zu Datenklassen ist ein Kinderspiel. Dies ist wahrscheinlich einer der häufigsten Gründe, sie überhaupt zu verwenden.
Giorgio Balestrieri
6

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.

jsonpickleist 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 defaultund object_hookArgumente für json.dump[s]()und 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

oder erstellen Sie Instanzen der Klassen JSONEncoderund JSONDecodermit 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.

Martijn Pieters
quelle
3

Wenn Sie keine zusätzlichen Module verwenden, können Sie die __post_init__Funktion verwenden, um die dictWerte 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 strFunktion wieder umgewandelt werden kann, ist es faires Spiel. Zum Beispiel kann es verwendet werden, um strFelder wie IP4Addressintern zu halten .

Killjoy
quelle
0

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)
Tobias Hermann
quelle
0

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.

Zah
quelle
0
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

Evgeniy_Burdin
quelle
-1

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.

NOOBAF
quelle
Diese Lösung ist schlecht, es ist zu kompliziert, Sie sollten lieber eine externe Bibliothek wie "Dacite" verwenden.
Jurass
@jurass Wenn das OP eine Bibliothek benutzen wollte, würde er diese Frage nicht stellen
NOOBAF