Version en ligne

Tutoriel : Sérialisez vos objets au format JSON !

Table des matières

Sérialisez vos objets au format JSON !
Présentation du format JSON
Présentation du module
Sérialisons nos propres classes !
Annexe

Sérialisez vos objets au format JSON !

Présentation du format JSON

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.

Présentation du format JSON

Présentation du module

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 :

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

<?xml version="1.0" ?>
<playlist nom="MeshowRandom">
    <chanson>
        <titre>Best Improvisation Ever 2</titre>
        <auteur>David Meshow</auteur>
        <note>5</note>
    </chanson>
    <chanson>
        <titre>My Theory (Bonus)</titre>
        <auteur>David Meshow</auteur>
        <note>4</note>
    </chanson>
</playlist>

Et en JSON, ça donne quoi ?

Voici les mêmes informations contenues dans un fichier JSON.

{
    "nom" : "MeshowRandom",
    
    "chansons" : [
        {
            "titre" : "Best Improvisation Ever 2",
            "auteur" : "David Meshow",
            "note" : 5
        },     
        {
            "titre" : "My Theory",
            "auteur" : "David Meshow",
            "note" : 4
        }
    ]
}

Bilan

À partir de cette comparaison, on peut clairement voir émerger l'avantage principal du format JSON par rapport au format XML : il est minimaliste.


Présentation du module

Présentation du module

Présentation du format JSON Sérialisons nos propres classes !

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

Tout d'abord, on inclut le module JSON :

>>> import json

Ensuite, on peuple la playlist :

>>> playlist = {}
>>> playlist["nom"] = "MeshowRandom"
>>> playlist["musiques"] = []
>>> playlist["musiques"].append("Best Improvisation Ever 2")
>>> playlist["musiques"].append("My Theory (Bonus)")
>>> 
>>> print(playlist)
{'musiques': ['Best Improvisation Ever 2', 'My Theory (Bonus)'], 'nom': 'MeshowRandom'}

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"
}

C'est beau, n'est-ce pas ?

Voici un exemple :

>>> from collections import OrderedDict
>>> dct = OrderedDict()
>>> dct["__class__"] = "Playlist"
>>> dct["name"] = "MeshowRandom"
>>> dct["description"] = "Cool stuff."
>>>
>>> import json
>>> print(json.dumps(dct, indent="4"))
{
    "__class__": "Playlist",
    "name": "MeshowRandom",
    "description": "Cool stuff."
}

Et après ?

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


Présentation du format JSON Sérialisons nos propres classes !

Sérialisons nos propres classes !

Présentation du module Annexe

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 :

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 :

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 !


Présentation du module Annexe

Annexe

Sérialisons nos propres classes !

Sérialiser plusieurs objets

Contexte

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 :

pl1 = Playlist("MeshowRandom")
pl1.musiques.append("Best improvisation ever 2")
pl1.musiques.append("My theory")

pl2 = Playlist("MeshowRandom2")
pl2.musiques.append("Corolla song")
pl2.musiques.append("Best guitar improvisation ever")

liste_playlists = []
liste_playlists.append(pl1)
liste_playlists.append(pl2)

with open('Test.json', 'w', encoding='utf-8') as f:
    json.dump(liste_playlists, f, indent=4, default=serialiseur_perso)

Et voici ce que ça donne :

[
    {
        "musiques": [
            "Best improvisation ever 2", 
            "My theory"
        ], 
        "nom": "MeshowRandom", 
        "__class__": "Playlist"
    }, 
    {
        "musiques": [
            "Corolla song", 
            "Best guitar improvisation ever"
        ], 
        "nom": "MeshowRandom2", 
        "__class__": "Playlist"
    }
]

Sérialiser plusieurs classes différentes

Contexte

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 :

playlist = Playlist("MeshowRandom")
playlist.musiques.append(Musique("Best Improvisation Ever 2", "David Meshow", 5))
playlist.musiques.append(Musique("My Theory", "David Meshow", 4))

with open('Test.json', 'w', encoding='utf-8') as f:
    json.dump(playlist, f, indent=4, default=serialiseur_perso)

Et enfin le contenu du fichier :

{
    "musiques": [
        {
            "note": 5, 
            "titre": "Best Improvisation Ever 2", 
            "auteur": "David Meshow", 
            "__class__": "Musique"
        }, 
        {
            "note": 4, 
            "titre": "My Theory", 
            "auteur": "David Meshow", 
            "__class__": "Musique"
        }
    ], 
    "nom": "MeshowRandom", 
    "__class__": "Playlist"
}

Utilisation avec l'héritage

Contexte

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 :

>>> import copy
>>> obj = ImagedPlaylist("MeshowRandom")
>>>
>>> obj_cpy = copy.copy(obj)
>>> obj_cpy.__class__ = Playlist
>>>
>>> print(obj)
<__main__.ImagedPlaylist object at 0x0000000003380630>
>>> print(obj_cpy)
<__main__.Playlist object at 0x0000000003385DD8>

Ainsi nous allons pouvoir directement le rajouter dans notre fonction serialiseur_perso :

import copy
def serialiseur_perso(obj):
    if isinstance(obj, ImagedPlaylist):
        obj_cpy = copy.copy(obj)
        obj_cpy.__class__ = Playlist
        return {"__class__": "ImagedPlaylist",
                "image": obj.image,
                "__parent__": obj_cpy}
    
    if isinstance(obj, Playlist):
        return {"__class__": "Playlist",
                "nom": obj.nom,
                "musiques": obj.musiques}

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 :

{
    "image": "Test.png", 
    "__class__": "ImagedPlaylist", 
    "__parent__": {
        "musiques": [], 
        "nom": "MeshowRandom", 
        "__class__": "Playlist"
    }
}

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

Sources :

Remerciements :


Sérialisons nos propres classes !