[PYTHON] JSON-Codierung / Decodierung von benutzerdefinierten Objekten

Überblick

Beschreibt ein Beispiel für die JSON-Codierung / -Decodierung komplexer Strukturen, die Instanzen (Objekte) benutzerdefinierter Klassen enthalten.

Die Sprache ist Python 3.8.1.

Für Python ist es einfacher, "pickle" zum Serialisieren komplexer Daten zu verwenden. Wenn Sie jedoch außerhalb von Python lesen oder schreiben möchten oder nach der Serialisierung eine gewisse Lesbarkeit wünschen, wählen Sie "JSON". Häufig.

Ich denke, es gibt andere Wege als die hier beschriebenen, aber ich mag diesen auf folgende Weise:

  1. Verwenden Sie den Python-Standard json-Modul.
  2. Die Codierungs- / Decodierungslogik kann in Klassen unterteilt werden.

Benutzerdefinierte und komplexe Datenbeispiele

Da es zur Erklärung verwendet wird, wird es nicht so kompliziert sein, aber ich werde es mit Daten versuchen, die die folgenden Bedingungen erfüllen.

  1. Es sind mehrere benutzerdefinierte Klassen enthalten.
  2. Das benutzerdefinierte Objektattribut enthält auch das benutzerdefinierte Objekt.
class Person:
    def __init__(self, name):
        self.name = name

class Robot:
    def __init__(self, name, creator=None):
        self.name = name
        self.creator = creator

alan = Person('Alan')
beetle = Robot('Beetle', creator=alan)
chappy = Robot('Chappy', creator=beetle)

"Alan" ist ein Mensch und "Käfer" und "Chappy" sind Roboter. Im Folgenden möchte ich eine Liste von Roboterdaten erstellen und diese Liste codieren / decodieren.

robots = [beetle, chappy]

Kodieren

Das Serialisieren eines Objekts in eine JSON-Zeichenfolge wird als ** Codierung ** bezeichnet. Diese Liste enthält Objekte der Klassen "Person" und "Roboter", daher müssen Sie sie codieren können.

Einfache Codierung

Beginnen wir mit einer einfachen Kodierung der Person-Klasse.

Bestimmen Sie die Codierungsspezifikationen

Um ein benutzerdefiniertes Objekt zu codieren, müssen Sie entscheiden, wie es codiert werden soll (die Spezifikation).

Hier werden ** Klassenname ** und ** Attributinhalt ** als Name-Wert-Paare ausgegeben. Im obigen Fall von "alan" wird angenommen, dass die JSON-Zeichenfolge wie folgt lautet.

{"class": "Person", "name": "Alan"}

Erstellen Sie einen benutzerdefinierten Encoder

Verwenden Sie einen benutzerdefinierten Encoder, indem Sie den Parameter cls in der Standardfunktion [json.dumps] angeben (https://docs.python.org/ja/3/library/json.html#json.dumps). Ich kann. Benutzerdefinierte Encoder werden erstellt, indem json.JSONEncoder geerbt und die Standardmethode überschrieben wird. .. Da das Objekt im Argument der default -Methode enthalten ist, ist es in Ordnung, wenn Sie es in einer Form zurückgeben, die von json.JSONEncoder verarbeitet werden kann (hier dict einschließlich nur str).

import json

class PersonEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Person):
            return {'class': 'Person', 'name': obj.name}
        else:
            return super().default(obj)

print(json.dumps(alan, cls=PersonEncoder))

#Ergebnis:
{"class": "Person", "name": "Alan"}

Komplexe Codierung

Als nächstes erstellen wir einen Encoder der Klasse "Robot", aber das ist nicht kompliziert. Wie ich in der "Übersicht" geschrieben habe, ** ist die Codierungslogik durch die Klasse ** getrennt.

class RobotEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Robot):
            return {'class': 'Robot', 'name': obj.name, 'creator': obj.creator}
        else:
            return super().default(obj)

Es ist fast dasselbe wie "PersonEncoder". Es läuft jedoch nicht so wie früher mit "PersonEncoder". Dies liegt daran, dass der "Ersteller" im Rückgabewert nicht in einer Form vorliegt, die von "json.JSONEncoder" verarbeitet werden kann. Ich wage es, die Logik auf diese Weise zu teilen und beim tatsächlichen Codieren die beiden Codierer zusammen zu verwenden.

Kombinieren Sie den Encoder

Erstellen Sie eine neue Klasse mit Mehrfachvererbung, um die Encoder zusammenzuführen.

class XEncoder(PersonEncoder, RobotEncoder):
    pass

print(json.dumps(robots, cls=XEncoder))

#Ergebnis:
[{"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}, {"class": "Robot", "name": "Chappy", "creator": {"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}}]
Wenn das Ergebnis formatiert ist (zum Anzeigen klicken).
print(json.dumps(robots, cls=XEncoder, indent=4))

#Ergebnis:
[
    {
        "class": "Robot",
        "name": "Beetle",
        "creator": {
            "class": "Person",
            "name": "Alan"
        }
    },
    {
        "class": "Robot",
        "name": "Chappy",
        "creator": {
            "class": "Robot",
            "name": "Beetle",
            "creator": {
                "class": "Person",
                "name": "Alan"
            }
        }
    }
]

Dies liegt daran, dass Sie in der Funktion "json.dumps" nur eine Encoderklasse angeben können. Sie kann jedoch auch erweitert werden, wenn die Anzahl der Objekttypen zunimmt.

(Ergänzung) Über die Operation durch Mehrfachvererbung

Ich werde kurz erklären, warum es funktioniert, indem ich den obigen "XEncoder" erstelle.

In Mehrfachvererbung von Python-Klassen wird auf die Attribute in der Reihenfolge der Vererbung verwiesen. Wenn Sie die "Standard" -Methode von "XEncoder" aufrufen, wechseln Sie zuerst zur "Standard" -Methode des geerbten "PersonEncoder".

Die Methode "PersonEncoder.default" gibt "dict" von selbst zurück, wenn "obj" ein "Person" -Objekt ist, andernfalls wird die Supermethode aufgerufen.

Die Supermethode in diesem Fall wäre RobotEncoder.default ** anstelle von ** json.JSONEncoder.default. Dies ist Pythons Mehrfachvererbungsbewegung.

Wenn RobotEncoder.default eine Super-Methode aufruft, erbt sie nicht mehr, sodass die Verarbeitung an die ursprüngliche Superklasse json.JSONEncoder delegiert wird.

Ich habe nicht untersucht, wie die "Standard" -Methode rekursiv aufgerufen wird, aber solange die "if" -Anweisung eine Klassenentscheidung trifft, scheint es, dass das gleiche Ergebnis erzielt werden kann, selbst wenn die Vererbungsreihenfolge umgekehrt wird.

Dekodieren

Das Deserialisieren einer JSON-Zeichenfolge in ein Objekt im Gegensatz zur Codierung wird als ** Decodierung ** bezeichnet. json.loads Durch Übergeben des Parameters object_hook an die Methode wird eine benutzerdefinierte Verarbeitung auf das decodierte Objekt angewendet. Kann hinzugefügt werden.

Einfaches object_hook Beispiel

Schauen wir uns zunächst ein Beispiel an, bei dem nur ein Objekt der Klasse "Person" codiert und dekodiert wird. Die als "object_hook" übergebene Funktion empfängt ein dekodiertes Objekt (z. B. "dict"). Schreiben Sie also, was zu tun ist, wenn der Wert von "class" "dict" ist, also "Person".

def person_hook(obj):
    if type(obj) == dict and obj.get('class') == 'Person':
        return Person(obj['name'])
    else:
        return obj

#In JSON-Zeichenfolge codieren
alan_encoded = json.dumps(alan, cls=PersonEncoder)
#Aus JSON-String dekodieren
alan_decoded = json.loads(alan_encoded, object_hook=person_hook)

print(alan_decoded.__class__.__name__, vars(alan_decoded))

#Ergebnis:
Person {'name': 'Alan'}

Kombinieren Sie object_hook

Erstellen Sie als Nächstes einen object_hook für die Robot-Klasse und erstellen Sie eine neue Funktion, die beide kombiniert.

def robot_hook(obj):
    if type(obj) == dict and obj.get('class') == 'Robot':
        return Robot(obj['name'], creator=obj['creator'])
    else:
        return obj

def x_hook(obj):
    return person_hook(robot_hook(obj))

Die kombinierte Funktion "x_hook" kann auch wie folgt geschrieben werden: Es wird etwas länger dauern, aber es ist einfacher, die Anzahl der Haken zu erhöhen (die Reihenfolge des Anbringens von Haken unterscheidet sich vom obigen Beispiel, aber es gibt kein Problem).

def x_hook(obj):
    hooks = [person_hook, robot_hook]
    for hook in hooks:
        obj = hook(obj)
    return obj

Verwenden wir dies, um die Liste der oben erstellten Roboter zu codieren / decodieren.

#In JSON-Zeichenfolge codieren
robots_encoded = json.dumps(robots, cls=XEncoder)
#Aus JSON-String dekodieren
robots_decoded = json.loads(robots_encoded, object_hook=x_hook)

for robot in robots_decoded:
    print(robot.__class__.__name__, vars(robot))

#Ergebnis:
Robot {'name': 'Beetle', 'creator': <__main__.Person object at 0x0000029A48D34CA0>}
Robot {'name': 'Chappy', 'creator': <__main__.Robot object at 0x0000029A48D38100>}

Wie bei der Codierung (wahrscheinlich, weil sie von innen rekursiv decodiert wird) hat das Ändern der Reihenfolge, in der die Hooks angewendet werden, das Ergebnis nicht geändert.

(Ergänzung) Die Codierung kann auf die gleiche Weise angepasst werden.

Tatsächlich kann die Codierungsseite angepasst werden, indem eine Funktion auf die gleiche Weise zugewiesen wird. Im Gegenteil, wenn Sie versuchen, die Decodierungsseite zu einer Unterklasse des Decoders zu machen, wird dies komplizierter.

Wenn Sie eine benutzerdefinierte Codierungslogik zusammenführen, ist es besser, die Methode zum Erstellen einer Unterklasse zu wählen, wenn Sie nur mit Mehrfachvererbung schreiben möchten, und die Methode zum Zuweisen einer Funktion, wenn Sie dem Stil auf der Decodierungsseite entsprechen möchten.

Ein Beispiel für das Anpassen der Codierungsseite durch Angabe einer Funktion

def person_default(obj):
    if isinstance(obj, Person):
        return {'class': 'Person', 'name': obj.name}
    else:
        return obj

def robot_default(obj):
    if isinstance(obj, Robot):
        return {'class': 'Robot', 'name': obj.name, 'creator': obj.creator}
    else:
        return obj

def x_default(obj):
    defaults = [person_default, robot_default]
    for default in defaults:
        obj = default(obj)
    return obj

print(json.dumps(robots, default=x_default))

#Ergebnis:
[{"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}, {"class": "Robot", "name": "Chappy", "creator": {"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}}]

Aufgabe

Es gibt einige Probleme bei der Dekodierung. Im obigen Beispiel waren der erste dekodierte Roboter 'Beetle' und der'Chappy''Creator''Beetle 'ursprünglich dasselbe Objekt. Auch der "Schöpfer" dieser "Beelte", "Alan", war das gleiche Objekt.

Das obige Decodierungsverfahren reproduziert die Situation vor dem Codieren nicht vollständig, da es nicht bedeutet, dass das bereits erstellte Objekt erneut verwendet wird, weil es denselben Namen hat. Wenn Sie dies tun möchten, können Sie einen Mechanismus für die Klassen "Person" und "Roboter" erstellen, sodass Sie das entsprechende Objekt von "object_hook" erhalten können, indem Sie einfach den Namen angeben.

Recommended Posts

JSON-Codierung / Decodierung von benutzerdefinierten Objekten
JSON-Codierung und -Decodierung mit Python
Objektäquivalenzbeurteilung in Python