Python : les fichiers (3/3) – La sérialisation des données avec le module pickle

Pour une lecture plus agréable (page plus large), je vous invite à cliquer sur ce lien et à lire ce chapitre dans la rubrique consacrée au langage Python.

Dans ce chapitre, nous allons découvrir un module très intéressant puisqu’il permet d’enregistrer des données dans un fichier binaire et de les restituer ultérieurement avec leur type initial.

En effet, jusqu’ici, dans les deux chapitres précédents, nous n’avons enregistré que des données de type string dans les fichiers que nous avons créés. Si nous voulions par exemple enregistrer le nombre entier 9, il fallait d’abord convertir ce dernier en chaîne de caractères (ligne n° 3) et effectuer ensuite l’opération inverse pour pouvoir l’additionner avec un autre nombre entier (ligne n° 6).

number = 9
with open("mon_fichier", 'w') as file_:
    file_.write(str(number))
with open("mon_fichier", 'r') as file_:
    number = file_.readline()
    result = 4 + int(number)
    print("4 + {} = {}".format(number, result))

Le module pickle

Mais ça, c’était avant!… Avant de découvrir le module pickle qui va nous permettre d’enregistrer dans un fichier binaire, des nombres entiers (type int), des nombres décimaux (type float), des listes (type list), des dictionnaires (type dict) et même des objets que l’on a soi-même instanciés! Ce procédé, d’une grande utilitude, porte un nom : la sérialisation des données. Voici comment cela fonctionne. J’ai commenté chaque ligne de ce code (un peu plus bas):

import pickle

int_number = 23
float_number = 15.89
first_names = ["Pamphile", "Théophane", "Esclarmonde"]
dictionary = {"Pamphile" : 32, "Théophane" : 54, "Esclarmonde" : 45}

class First_name(object):
    def __init__(self, first_names):
        self.p = first_names[0]
        self.t = first_names[1]
        self.e = first_names[2]
    def majuscules(self):
        return "{}, {} et {}".format(self.p.upper(), self.t.upper(), self.e.upper())

upper_names = First_name(first_names)

with  open("donnees_enregistrees", 'wb') as file_:
    pickle.dump(int_number, file_)
    pickle.dump(float_number, file_)
    pickle.dump(first_names, file_)
    pickle.dump(dictionary, file_)
    pickle.dump(upper_names, file_)

with  open("donnees_enregistrees", 'rb') as file_:
    i = pickle.load(file_)
    print(i, type(i))
    j = pickle.load(file_)
    print(j, type(j))
    k = pickle.load(file_)
    print(k, type(k))
    l = pickle.load(file_)
    print(l, type(l))
    m = pickle.load(file_)
    print(m.majuscules(), type(m))
    n = pickle.load(file_)
    print(n, type(n))

Exécution du code :

23 <class ‘int’>
15.89 <class ‘float’>
[‘Pamphile’, ‘Théophane’, ‘Esclarmonde’] <class ‘list’>
{‘Théophane’: 54, ‘Pamphile’: 32, ‘Esclarmonde’: 45} <class ‘dict’>
PAMPHILE, THÉOPHANE et ESCLARMONDE <class ‘__main__.First_name’>
Traceback (most recent call last):
File « /home/benoit/test_pickle.py », line 35, in
n = pickle.load(file_)
EOFError: Ran out of input

Enregistrement des données

Ligne n° 1 : Importation du module pickle

Lignes n° 3 à 6 : création de quatre objets de type différents (int, float, list et dict)

Lignes n° 8 à 14 : Définition d’une classe First_name avec la méthode majuscules() qui permet de mettre en majuscules, une liste de prénoms passée en argument

Ligne n° 16 : Création de l’objet upper_names par instanciation de la classe First_name.

Ligne n° 18 : Création et ouverture d’un fichier en mode ‘wb’ c’est-à-dire en mode écriture et binaire. Le fichier s’appelle « donnees_enregistrees »

Lignes n° 19 à 23 : Enregistrement dans le fichier, de données de cinq types différents, à savoir int, float, list, dict et objet personnel. Cet enregistrement s’effectue grâce à la fonction dump() du module pickle. Cette fonction prend deux arguments qui sont la variable à enregistrer et l’objet correspondant au fichier cible (L’objet file_ correspond au fichier donnees_enregistrees)

À ce stade, vous pouvez vous amuser à ouvrir le fichier binaire « donnees_enregistrees », vous obtiendrez un incompréhensible baragouin où surnagent quelques prénoms qui n’ont pas encore sombré dans les abysses de la conscience-machine.

donnees_enregistrees

Restitution des données

Ligne n° 25 : Ouverture du fichier « donnees_enregistrees » en mode ‘rb’ c’est-à-dire en mode lecture et binaire. C’est la fonction load() du module pickle qui va nous permettre de restituer chaque donnée en respectant leur type d’origine! La fonction load() prend un argument qui est l’objet (file_) correspondant au fichier « donnees_enregistrees ».

Ligne n° 26 : restitution de l’objet correspondant au nombre entier 23 que nous stockons dans la variable i.

Ligne n° 27 : Le print de i et de son type nous confirme qu’il s’agit bien du nombre entier 23.

Lignes n° 28 à 33 : Même opération avec les variables j, k et l qui récupèrent respectivement un objet de type float (15.89), une liste et un dictionnaire.

Ligne n°34 : Ici, c’est un peu particulier… La variable m stocke l’objet que nous avons nous-mêmes créé par instanciation de la classe First_name.

Ligne n° 35 : Nous faisons un print() de m.majuscules(), c’est-à-dire que j’applique la méthode majuscules(), définie dans la classe First_name, sur l’objet m. Le résultat qui s’affiche est PAMPHILE, THÉOPHANE et ESCLARMONDE.

Ligne n° 36 : Toutes les données ont été restituées si bien que la dernière variable (n) n’a plus rien à stocker. C’est la raison pour laquelle Python nous retourne une exception (EOFError: Ran out of input).

Découper une base de données en plusieurs fichiers portant l’extension .pkl

Nous pouvons bien évidemment modifier les valeurs extraites et les enregistrer de nouveau à l’aide de la fonction dump(). Cela dit, si la base de données est très importante, le processus risque d’être un peu lourd et par conséquent, il s’en trouvera fortement ralenti.

Une solution consiste à découper la base de données en plusieurs fichiers portant l’extension ‘.pkl’. Analysons le code ci-dessous:

import pickle
import glob

int_number = 23
float_number = 15.89
first_names = ["Pamphile", "Théophane", "Esclarmonde"]
dictionary = {"Pamphile" : 32, "Théophane" : 54, "Esclarmonde" : 45}

class First_name(object):
    def __init__(self, first_names):
        self.p = first_names[0]
        self.t = first_names[1]
        self.e = first_names[2]
    def majuscules(self):
        return "{}, {} et {}".format(self.p.upper(), self.t.upper(), self.e.upper())

upper_names = First_name(first_names)

objects_list = [("int_number", int_number),
                ("float_number", float_number),
                ("first_names", first_names),
                ("dictionary", dictionary),
                ("upper_names", upper_names)]

for (x, y)in objects_list:
    with open(x + '.pkl', 'wb')as file_:
        pickle.dump(y, file_)

for f in glob.glob('*.pkl'):
    with open(f, 'rb') as file_:
        data = pickle.load(file_)
        if type(data) == dict:
            print(data)
            data['Théophane'] = 89
            print(data)
with open(f, 'wb') as file_:
pickle.dump(data, file_)

Exécution du code :

{‘Théophane’: 54, ‘Pamphile’: 32, ‘Esclarmonde’: 45}
{‘Théophane’: 89, ‘Pamphile’: 32, ‘Esclarmonde’: 45}

Ligne n° 2 : importation du module glob qui va nous permettre de faire l’inventaire du répertoire courant (/home/benoit) en lui appliquant un filtre (en l’occurrence l’extension *.pkl)

Ligne n° 19 : Nous créons une liste de tuples contenant les cinq objets de type différent que nous avons précédemment instanciés, associés au nom de leur futur fichier .pkl .

Lignes n° 25 à 27 : À l’aide d’une boucle for, nous créons le chemin de chaque fichier (x + ‘.pkl’) et nous enregistrons les données qui lui correspondent (y).

Ligne n° 29 : Nous faisons appel au module glob pour lister les fichiers portant l’extension .pkl.

Ligne n° 31 : Extraction des données

Ligne n° 32 : Nous créons une condition (Si les données sont de type dictionnaire…).

Ligne n° 34 : Nous modifions l’âge de Théophane.

Lignes n° 35 et 36 : Nous enregistrons les données modifiées dans le fichier binaire.

Au bout du compte, nous n’avons déchargé et chargé que les données que nous souhaitions modifier.

Publicités

Auteur : Ordinosor

Bienvenue sur Miamondo, mon blog personnel. "Mia mondo", c'est de l'espéranto et ça signifie "Mon monde" en français. Je m'appelle Benoît alias Ordinosor, Français expatrié en Allemagne. Mes centres d'intérêt sont les distributions GNU/Linux, le langage de programmation Python, la science-fiction et l'espéranto.

6 réflexions sur « Python : les fichiers (3/3) – La sérialisation des données avec le module pickle »

  1. Il me semble important de noter quelque part que ces fichiers pickle ne doivent en aucun cas venir de l’extérieur du système ou être modifiable par un utilisateur. En effet ils contiennent ultimement du code qui est exécuté, donc s’ils étaient modifiés avec des intentions malicieuses la sécurité de l’ordinateur serait fortement impactée.

    Ainsi pour la sérialisation de données en transit il vaut mieux préférer un format neutre comme le json par exemple.

    J'aime

  2. Pour compléter ce que dit Cym13, je dirais aussi que le format pickle ne devrait être utilisé que pour des données qui ne peuvent pas être stockées autrement.

    Dans ton exemple, où tu stockes uniquement des chaînes et des nombres, des formats comme le JSON ou le YAML sont bien plus appropriés : c’est beaucoup plus sûr (les fichiers ne contiennent que des données, pas des objets Python pouvant être exécutables), c’est beaucoup plus lisible (tu peux ouvrir et modifier manuellement le fichier sans aucun problème) et plus portable (la plupart des langages de programmation supportent JSON et YAML, soit nativement, soit via une bibliothèque externe).

    Par exemple, ton dico {‘Théophane’: 54, ‘Pamphile’: 32, ‘Esclarmonde’: 45} deviendra :
    * {‘Th\\xE9ophane’: 54, ‘Pamphile’: 32, ‘Esclarmonde’: 45}
    * {Esclarmonde: 45, Pamphile: 32, « Th\\xE9ophane »: 54}

    Tu remarqueras au passage que les syntaxes sont très proches de celles utilisées en Python pour déclarer un dictionnaire.

    Il y a juste quelques limitations, qui se contournent facilement en concevant correctement son modèle de données.
    Par exemple, en JSON une clé de dictionnaire est forcément une chaîne, donc si tu dump un dico utilisant des clés numériques puis le réimporte, tu récupéres des clés sous forme de chaîne dict({0: « toto », « 1 »: « tata »}) devient {« 0 »: « toto », « 1 »: « tata »} en JSON et donc dict({« 0 »: « toto », « 1 »: « tata »}) une fois réimporté en Python. YAML gère par contre ça correctement, dans le dump il ne met par défaut pas de  » autour des chaînes, mais en ajoute dès qu’il y a ambiguïté, donc quand on a une chaîne constituée uniquement de chiffres, il va mettre des guillemets pour bien la distinguer du nombre. Ainsi, dict({0: « toto », « 1 »: « tata »}) devient {0: toto, « 1 »: tata} en YAML, et redevient dict({0: « toto », « 1 »: « tata »}) une fois réimporté.

    Et pour avoir encore plus de souplesse, tu peux passer à des bases de données. Python supporte nativement les bases DBM (sauf sous Windows) et les bases SQLite. C’est surtout utile si tu commences à avoir des volumes de données important, parce que ça te permet de ne charger et modifier que ce dont tu as besoin, sans toucher au reste, alors que pickle, JSON et YAML fonctionnent obligatoirement à l’échelle d’un fichier entier.

    J'aime

    1. Bonjour Matt,

      Merci pour ce long commentaire et toutes ces précisions concernant le JSON et la sécurité des données. Du coup, il va falloir que je m’y intéresse de près et que je fasse des recherches pour écrire un nouvel article sur ce sujet au plus vite! 🙂
      J’ai cependant une question. Dans un des codes de mon article, je « dumpe » un objet instancié par une classe que j’ai moi-même créée. Est-ce que dans cet exemple également le module pickle est moins approprié que JSON en termes de sécurité?

      J'aime

      1. Oui, ça reste moins approprié, car au moment où tu recharges ton fichier pickle, tu n’as pas la main sur le type des objets qui vont être rechargés (puisque ça dépend du contenu du fichier, qui peut avoir été altéré).

        Alors qu’avec du JSON ou du YAML, ce qui sera instancié à la lecture ne peut être que des types « simples » (nombres, chaînes, listes, dictionnaires), et c’est ensuite à toi d’interpréter ces données si tu veux produire d’autres types. Ça nécessite un peu plus de code, aussi bien pour gérer le dump que pour gérer le chargement, mais par contre ça t’offre bien plus de sécurité et de souplesse.

        Par exemple, pour ta classe First_names, tu pourrais ajouter dessus une fonction « get_as_list » qui ferait un return [self.p, self.t, self.e], et pour dumper ton objet upper_names en JSON/YAML, tu ferais un json/yaml.dump(upper_names.get_as_list()). Et à la lecture du JSON, tu récupérerai donc une liste, à passer au constructeur de First_names pour recréer un objet First_names. Tu pourrais aussi utiliser upper_names.__dict__, en adaptant le constructeur de First_names pour qu’il accepte un dico en entrée, et pas seulement une liste.

        Et même en dehors de toute considération de sécurité, quand on stocke des données, c’est en règle générale préférable de les stocker dans un format le plus simple et universel possible, parce que tu ne sais jamais quand et comment tu pourrais être amené à devoir manipuler les données à l’avenir (imagine par exemple le cas extrême où tu perdrais le code source de ton logiciel… un JSON, un YAML ou une base SQLite, ça sera toujours exploitable, alors que le pickle ne le sera plus).

        J'aime

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s