[PYTHON] Encodage / décodage JSON d'objets personnalisés

Aperçu

Décrit un exemple de codage / décodage JSON de structures complexes contenant des instances (objets) de classes personnalisées.

Le langage est Python 3.8.1.

Pour Python, il est plus facile d'utiliser pickle pour sérialiser des données complexes, mais si vous voulez lire ou écrire en dehors de Python, ou si vous voulez une certaine lisibilité après la sérialisation, choisissez JSON. Souvent.

Je pense qu'il existe d'autres moyens que ceux décrits ici, mais j'aime celui-ci des manières suivantes:

  1. Utilisez le standard Python module json.
  2. La logique de codage / décodage peut être divisée en classes.

Exemples de données personnalisées et complexes

Puisqu'il est utilisé pour l'explication, ce ne sera pas si compliqué, mais je vais l'essayer avec des données qui remplissent les conditions suivantes.

  1. Plusieurs classes personnalisées sont incluses.
  2. L'attribut d'objet personnalisé contient également l'objet personnalisé.
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» est un humain et «scarabée» et «chappy» sont des robots. Ci-dessous, je voudrais faire une liste de données de robot et encoder / décoder cette liste.

robots = [beetle, chappy]

Encoder

La sérialisation d'un objet dans une chaîne JSON s'appelle ** encoding **. Cette liste contient des objets des classes «Person» et «Robot», vous devez donc être en mesure de les encoder.

Encodage simple

Commençons par un simple encodage de classe Person.

Déterminer les spécifications de codage

Pour encoder un objet personnalisé, vous devez décider comment l'encoder (la spécification).

Ici, ** nom de classe ** et ** contenu d'attribut ** sont générés sous forme de paires nom-valeur. Dans le cas de alan ci-dessus, on suppose que la chaîne JSON sera la suivante.

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

Créer un encodeur personnalisé

Utilisez un encodeur personnalisé en spécifiant le paramètre cls dans la [fonction json.dumps] standard (https://docs.python.org/ja/3/library/json.html#json.dumps) Je peux. Les encodeurs personnalisés sont créés en héritant de json.JSONEncoder et en remplaçant la méthode default. .. Puisque l'objet est inclus dans l'argument de la méthode default, c'est OK si vous le renvoyez sous une forme qui peut être gérée par json.JSONEncoder (ici, dict incluant seulement 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))

#résultat:
{"class": "Person", "name": "Alan"}

Encodage complexe

Ensuite, nous allons créer un encodeur de la classe Robot, mais ce n'est pas compliqué. Comme je l'ai écrit dans "Vue d'ensemble", ** la logique de codage est séparée par classe **.

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)

C'est presque la même chose que «PersonEncoder». Cependant, cela ne se passe pas comme avec PersonEncoder plus tôt. C'est parce que le "créateur" dans la valeur de retour n'est pas sous une forme qui peut être gérée par "json.JSONEncoder". J'ose diviser la logique de cette manière, et lors de l'encodage, utiliser les deux encodeurs ensemble.

Combinez l'encodeur

Pour fusionner les encodeurs, créez une nouvelle classe à l'aide de l'héritage multiple.

class XEncoder(PersonEncoder, RobotEncoder):
    pass

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

#résultat:
[{"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}, {"class": "Robot", "name": "Chappy", "creator": {"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}}]
Lorsque le résultat est formaté (cliquez pour afficher).
print(json.dumps(robots, cls=XEncoder, indent=4))

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

C'est parce que vous ne pouvez spécifier qu'une seule classe d'encodeur dans la fonction json.dumps, mais elle peut être étendue même si le nombre de types d'objets augmente.

(Supplément) À propos de l'opération par héritage multiple

Je vais expliquer brièvement pourquoi cela fonctionne en créant le XEncoder ci-dessus.

Dans Héritage multiple de classes Python, les attributs sont référencés dans l'ordre d'héritage. Lorsque vous appelez la méthode «default» de «XEncoder», vous accédez d'abord à la méthode «default» du «PersonEncoder» hérité.

La méthode PersonEncoder.default renverra dict par elle-même si obj est un objet Person, sinon elle appellera la super-méthode.

La super méthode dans ce cas serait RobotEncoder.default ** au lieu de ** json.JSONEncoder.default. C'est le mouvement d'héritage multiple de Python.

Si RobotEncoder.default appelle une super méthode, elle n'hérite plus, donc le traitement est délégué à la superclasse d'origine json.JSONEncoder.

Je n'ai pas étudié comment la méthode default est appelée récursivement, mais tant que l'instruction if prend une décision de classe, il semble que le même résultat puisse être obtenu même si l'ordre d'héritage est inversé.

Décoder

La désérialisation d'une chaîne JSON en un objet, par opposition à l'encodage, est appelée ** décodage **. json.loads En passant le paramètre object_hook à la méthode, un traitement personnalisé est appliqué à l'objet décodé. Peut être ajouté.

Exemple simple object_hook

Tout d'abord, regardons un exemple d'encodage uniquement d'un objet de la classe Person et de décodage. La fonction passée en tant que «object_hook» reçoit un objet décodé (tel que «dict»), alors écrivez ce qu'il faut faire si la valeur de «classe» est «dict» qui est «Personne».

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

#Encoder en chaîne JSON
alan_encoded = json.dumps(alan, cls=PersonEncoder)
#Décoder à partir de la chaîne JSON
alan_decoded = json.loads(alan_encoded, object_hook=person_hook)

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

#résultat:
Person {'name': 'Alan'}

Combiner object_hook

Ensuite, créez un object_hook pour la classe Robot et créez une nouvelle fonction qui combine les deux.

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))

La fonction combinée x_hook peut également être écrite comme suit: Ce sera un peu plus long, mais il est plus facile d'augmenter le nombre de crochets (l'ordre d'application des crochets est différent de l'exemple ci-dessus, mais il n'y a pas de problème).

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

Utilisons ceci pour encoder / décoder la liste des robots créés ci-dessus.

#Encoder en chaîne JSON
robots_encoded = json.dumps(robots, cls=XEncoder)
#Décoder à partir de la chaîne JSON
robots_decoded = json.loads(robots_encoded, object_hook=x_hook)

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

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

Comme pour le codage (probablement parce qu'il est décodé récursivement de l'intérieur), changer l'ordre dans lequel les crochets sont appliqués n'a pas changé le résultat.

(Supplément) L'encodage peut être personnalisé de la même manière.

En fait, le côté encodage peut être personnalisé en donnant une fonction de la même manière. Au contraire, si vous essayez de faire du côté décodage une sous-classe du décodeur, ce sera plus compliqué.

Lors de la fusion de la logique de codage personnalisée, il est préférable de choisir la méthode de création d'une sous-classe si vous souhaitez écrire uniquement avec l'héritage multiple et la méthode de donner une fonction si vous souhaitez faire correspondre le style du côté de décodage.

Un exemple de personnalisation du côté encodage en donnant une fonction

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))

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

Tâche

Il y a quelques problèmes de décodage. Dans l'exemple ci-dessus, le premier robot décodé «Beetle» et le «Créateur Heureux» étaient à l'origine le même objet. De plus, le "créateur" de ces "Belte", "Alan", était le même objet.

La méthode de décodage ci-dessus ne reproduit pas complètement la situation avant l'encodage car elle ne signifie pas "réutiliser l'objet déjà créé car il porte le même nom". Si vous voulez faire cela, vous pouvez créer un mécanisme pour les classes Person et Robot afin que vous puissiez recevoir l'objet approprié du object_hook simplement en spécifiant le nom.

Recommended Posts

Encodage / décodage JSON d'objets personnalisés
Encodage et décodage JSON avec python
Jugement d'équivalence d'objet en Python