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:
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.
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]
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.
Commençons par un simple encodage de classe Person
.
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"}
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"}
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.
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"}}}]
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.
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é.
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é.
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'}
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.
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.
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"}}}]
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.