Ah, la POO… Quel plaisir de créer et de manipuler des objets aussi facilement ! Mais comment les sauvegarder simplement ou les faire transiter sur un réseau ? En utilisant la sérialisation, pardi ! :pirate:
Dans le tutoriel officiel de ce site concernant Python, les auteurs abordent l'utilisation du module pickle pour mener à bien la sérialisation de vos classes. Je tiens à vous présenter le module JSON qui a le même objectif : sauvegarder et restaurer les attributs de vos classes.
Tout au long de ce tutoriel, on distinguera bien le format de fichier JSON représentant la manière dont laquelle sont organisées les données dans le fichier et le module Python json qui permet, quant à lui, de manipuler cette représentation de données.
Dans cette partie, je vais vous présenter le format JSON et ses spécificités.
Introduction au format JSON
Citation : Wikipédia
JSON (JavaScript Object Notation) est un format de données textuel, générique, dérivé de la notation des objets du langage ECMAScript. Il permet de représenter de l'information structurée.
Euh… J'ai lu « JavaScript Object Notation ». Pourquoi venir nous parler de JavaScript dans un tutoriel concernant Python ? C'est quoi cette histoire ? :o
En fait, JSON est un format permettant de représenter des données. On peut comparer son usage à celui du XML.
Présentation du format JSON par l'exemple
Je vais ici vous présenter deux exemples de fichiers représentant les caractéristiques d'une playlist : l'un en XML et l'autre en JSON. Voici sa représentation « humaine ».
La playlist nommée MeshowRandom est composée de :
Best Improvisation Ever 2 de David Meshow avec une note de 5/5 ;
My Theory de David Meshow avec une note de 4/5.
Voyons maintenant sa représentation « informatique » en examinant les fichiers correspondants à cette description.
Tout d'abord, en XML !
Le format de balisage XML est très répandu (le zCode est dérivé du XML !).
Eh bien tout d'abord, comme tout module, il faut l'inclure. Pour ce faire, rien de plus simple :
import json
Sachez que ce module est déjà capable de sérialiser tous les types standards de Python hormis les tuples et les entrées binaires (en même temps, pour un format texte…).
Premier exemple : sérialiser dans une chaîne de caractères
Voici à présent la partie intéressante : on va demander au module de transposer notre dictionnaire au format JSON. Pour cela, on va se servir de la fonction json.dumps(objet) (le s signifie ici string).
>>> print(json.dumps(playlist))
{"musiques": ["Best Improvisation Ever 2", "My Theory (Bonus)"], "nom": "MeshowRandom"}
Vous pouvez constater que la fonction nous retourne une chaîne de caractères décrivant bien notre playlist au format JSON !
Mais le code est sur une seule ligne et n'est pas indenté… C'est illisible ! :o
En effet, ce n'est pas très lisible pour nous, mortels. On peut heureusement procéder comme ceci pour indenter automatiquement la sortie de la fonction :
>>> print(json.dumps(playlist, indent=4))
{
"musiques": [
"Best Improvisation Ever 2",
"My Theory (Bonus)"
],
"nom": "MeshowRandom"
}
C'est déjà plus clair. :)
Deuxième exemple : sérialiser dans un fichier
Eh bien, il suffit de sauvegarder la chaîne retournée par json.dumps(objet) dans un fichier, non ? Pourquoi ce deuxième exemple ?
On pourrait le faire, mais il est plus pratique d'utiliser une fonction du module dédiée à l'écriture dans les fichiers : la fonction json.dump(objet, flux). Celle-ci va directement écrire dans le flux de données (ici notre fichier), sans passer par une chaîne de caractères intermédiaire.
On va conserver la playlist remplie précédemment et utiliser cette fonction :
>>> with open('Test.json', 'w', encoding='utf-8') as f:
... json.dump(playlist, f, indent=4)
Voici ce que j'obtiens :
{
"musiques": [
"Best Improvisation Ever 2",
"My Theory (Bonus)"
],
"nom": "MeshowRandom"
}
Désormais, vous savez sérialiser les principaux objets de Python au format JSON, et ce, dans des fichiers. Cependant, avant de vous quitter, j'aimerais aborder la sérialisation d'instances de classes inconnues du module json telles que la classe Playlist ou la classe Musique.
Pour cela, rendez-vous dans la partie suivante. ;)
Nous allons maintenant nous attaquer au vif du sujet : comment sérialiser nos classes avec le module json. Prenons cette classe comme exemple : la classe Playlist (encore et toujours :p ).
class Playlist:
def __init__(self, nom):
nom = nom
musiques = []
Sérialisation
Voici la fonction qui va sérialiser notre objet :
def serialiseur_perso(obj):
if isinstance(obj, Playlist):
return {"__class__": "Playlist"
"nom": obj.nom,
"musiques": obj.musiques}
raise TypeError(repr(obj) + " n'est pas sérialisable !")
Je vais vous décrire pas à pas ce que produit ce code :
d'abord, on vérifie que l'objet passé est bien de type Playlist ;
si l'objet est bien une instance de la classe Playlist, on retourne un type sérialisable par le module (ici un dictionnaire) contenant les attributs à sauvegarder de notre classe ;
sinon, on lance une exception disant que l'objet passé n'est pas sérialisable.
Ingénieux, non ?
Euh… c'est quoi, l'entrée "__class__" dans le dictionnaire ? Ça se mange ?
Il s'agit en fait d'une entrée qui n'est pas contenue dans l'objet en lui-même mais elle va nous permettre de savoir de quel type est la représentation JSON de l'objet. Vous comprendrez mieux son utilité lors de la désérialisation.
Désérialisation
Nous allons maintenant faire l'inverse de la fonction serialiser_json : convertir le dictionnaire obtenu par le module json en playlist :
def deserialiseur_perso(obj_dict):
if "__class__" in obj_dict:
if obj_dict["__class__"] == "Playlist":
obj = Playlist(objet["nom"])
obj.musiques = objet["musiques"]
return obj
return objet
Voici ce que fait le code :
on regarde si "__class__" est défini dans le dictionnaire obj_dict ;
si oui, on vérifie si "__class__" == "Playlist" et on retourne l'objet obj créé à l'aide du contenu du dictionnaire ;
sinon, on retourne le dictionnaire.
Utilisation
Sérialiser un objet
# On crée un objet de type Playlist et on le peuple.
playlist = Playlist("MeshowRandom")
playlist.musiques.append("Best improvisation ever 2")
playlist.musiques.append("My Theory")
# On l'enregistre dans un fichier JSON avec notre sérialiseur perso.
with open("MaPlaylist.json", "w", encoding="utf-8") as fichier:
json.dump(playlist, fichier, default=serialiseur_perso)
Désérialiser un objet
# On crée un objet vide.
playlist = Playlist("untitled")
# On le peuple à l'aide du fichier JSON.
with open("MaPlaylist.json", "r", encoding="utf-8") as fichier:
playlist = json.load(fichier, object_hook=deserialiseur_perso)
Ça y est, vous êtes désormais capable de sérialiser toutes vos classes dans des fichiers JSON !
Supposons que vous ne vouliez pas sérialiser une seule playlist mais plusieurs situées dans un tableau. Eh bien, étant donné que les tableaux Python sont sérialisables par défaut avec le module, il ne devrait pas y avoir de problème !
Résultat
On peuple deux playlists et on les ajoute dans un tableau puis on enregistre ce même tableau :
Supposons que vous vouliez sérialiser votre playlist qui contient elle-même des instances de la classe Musique :
class Playlist:
def __init__(self, nom):
self.nom = nom
self.musiques = []
class Musique:
def __init__(self, titre, auteur, note = 3):
self.titre = titre
self.auteur = auteur
self.note = note
Il va donc falloir étendre la fonction serialiseur_perso en lui indiquant comment sérialiser la classe Playlistet la classe Musique. Voici ce que ça donne :
def serialiseur_perso(obj):
# Si c'est une musique.
if isinstance(obj, Musique):
return {"__class__": "Musique",
"titre": obj.titre,
"auteur": obj.auteur,
"note": obj.note}
# Si c'est une playlist.
if isinstance(obj, Playlist):
return {"__class__": "Playlist",
"nom": obj.nom,
"musiques": obj.musiques}
# Sinon le type de l'objet est inconnu, on lance une exception.
raise TypeError(repr(obj) + " n'est pas sérialisable !")
Résultat
Voici le code d'exemple, dans lequel on va peupler la playlist d'instances de la classe Musique :
Supposons que certaines de vos playlists peuvent être imagées, voici alors ce que l'on pourrait avoir :
class Playlist:
def __init__(self, nom):
self.nom = nom
self.musiques = []
class ImagedPlaylist(Playlist):
def __init__(self, nom):
Playlist.__init__(self, nom)
self.image = ""
La classe ImagedPlaylist hérite donc de la classe Playlist, mais comment faire pour sérialiser le tout ?
Nous allons tout d'abord enregistrer les attributs de la classe ImagedPlaylist puis ensuite, dans un champ nommé "__parent__", nous transformeront cet objet de type ImagedPlaylist en objet de type Playlist (downcasting) pour pouvoir enregistrer les attributs hérités de l'objet parent (comme l'attribut musiques par exemple).
Voici le code Python qui vous permettra d'effectuer un downcasting :
import copy
def ImagedPlaylist2Playlist(obj):
obj_cpy = copy.copy(obj) # On copie l'objet
obj_cpy.__class__ = Playlist # On change son attribut __class__ pour le convertir
return obj_cpy
Pour les plus sceptiques, voici la preuve en image :
A la ligne 8, on va demander au module de sérialiser un objet de type Playlist, il va donc en interne rappeler cette fonction avec l'objet de type Playlist que nous venons de convertir !
Résultat
Le tout marche très bien, voici ce que l'on peut obtenir :
Nous voilà arrivés au terme de ce tutoriel. Vous pouvez à présent supprimer les parseurs faits maison de fichiers texte et utiliser le format JSON ainsi que son module Python sans modération. :p