Python et la persistance de données

Fichiers DBM

Description et utilisation

Le module anydbm [1] offre une solution pour des besoins simples de persistance en utilisant des fichiers plats. Il propose une solution standard indépendante de l’implémentation. Un fichier DBM s’utilise comme un dictionnaire, donc relativement simplement, seul l’initialisation est différente (ouverture du fichier). Les données sont manipulées comme des chaînes et accessibles à l’aide de clés.

La fonction open(name, 'c') ouvre le fichier nommé name ('c' signifie «en le créant s’il n’existe pas»).

>>> import anydbm
>>> dbmfile = anydbm.open('blabla', 'c')

La fonction close() ferme le fichier (requis ou non selon les implémentations, pour être sûr que les données sont sauvées il vaut mieux utiliser explicitement close).

>>> dbmfile.close()

La lecture et l’écriture se fait comme dans un dictionnaire, mais les clés sont obligatoirement des chaînes. L’exemple suivant crée deux entrées (foo et bar) dans le fichier DBM, les lit depuis ce fichier et ferme le fichier.

>>> dbmfile = anydbm.open('blabla', 'c')
>>> dbmfile['foo'] = 'perrier c foo'
>>> dbmfile['bar'] = 'cd bar ; more beer'
>>> print dbmfile['foo']
perrier c foo
>>> print dbmfile['bar']
cd bar ; more beer
>>> dbmfile.close()

La fonction has_key() test la présence d’une clé.

>>> dbmfile = anydbm.open('blabla', 'c')
>>> dbmfile.has_key('foo')
True

La fonction keys() donne la liste des clés du fichier. Obtenir les clé est un pré-requis à tout parcours d’un fichier DBM. Comme pour les dictionnaires, il est possible de parcourir les clés d’un fichier DBM avec un itérateur.

>>> dbmfile.keys()
['foo', 'bar']
>>> for key in dbmfile:
...     print key, dbmfile[key]
foo perrier c foo
bar cd bar ; more beer

La fonction len() donne le nombre d’entrées d’un fichier.

>>> len(dbmfile)
2

L’opérateur del permet de supprimer une entrée (ce qui suit le fonctionnement standard d’un dictionnaire).

>>> dbmfile.keys()
['foo', 'bar']
>>> del dbmfile['foo']
>>> dbmfile.keys()
['bar']
>>> len(dbmfile)
1
>>> dbmfile.close()

Limitations

Les fichiers DBM permettent uniquement de rendre persistant des chaînes de caractères. La conversion objet vers chaîne (et réciproquement) doit être gérée manuellement, ce qui rend leur utilisation rapidement complexe pour des objets composites.

Pickle et Shelve

Object pickling

Le module pickle (standard en Python) permet la sérialisation des objets mémoire en chaînes (et réciproquement). Il est utile pour la persistance et le transferts de données sur un réseau.

>>> import pickle

La manipulation des données utilise un fichier par sérialisation, où les données sont stockées sous forme de chaînes. Il n’y a pas de structuration des données (donc pas de recherche rapide possible).

(Dé)Sérialisation et fichiers

La classe Pickler est un sérialiseur vers un fichier et la fonction dump() réalise la sérialisation d’un objet (il existe aussi une fonction équivalente dans le module). L’exemple suivant sérialise un dictionnaire dans le fichier foo.saved.

>>> foo = {'a': 'aaa', 'b': 'bbb', 'c': 'ccc'}
>>> output = open('foo.saved', 'w')
>>> p = pickle.Pickler(output)                #(1)
>>> p.dump(foo)                               #(2)
>>> output.close()

Ce qui est équivalent à l’utilisation de la fonction dump du module.

>>> output = open('foo.saved', 'w')
>>> pickle.dump(foo, output)                  #(1,2)
>>> output.close()

La classe Unpickler est un dé-sérialiseur depuis un fichier et la fonction load() réalise la dé-sérialisation d’un objet (il existe aussi une fonction équivalente dans le module). L’exemple suivant recharge depuis le fichier foo.saved le dictionnaire sérialisé dans l’exemple précédent.

>>> input = open('foo.saved', 'r')
>>> p = pickle.Unpickler(input)              #(1)
>>> foo2 = p.load()                          #(2)
>>> input.close()
>>> foo2
{'a': 'aaa', 'c': 'ccc', 'b': 'bbb'}

Ce qui est équivalent à l’utilisation de la fonction load du module.

>>> input = open('foo.saved', 'r')
>>> foo2 = pickle.load(input)                #(1,2)
>>> input.close()

(Dé)Sérialisation et chaînes de caractères

La fonction dumps() sérialise un objet vers une chaîne (et non plus vers un fichier). Cette opération est pratique pour échanger des messages sur un réseau par exemple, ou bien pour les stocker dans un fichier DBM.

>>> data = pickle.dumps(foo)
>>> data
"(dp0\nS'a'\np1\nS'aaa'\np2\nsS'c'\np3\nS'ccc'\np4\nsS'b'\np5\nS'bbb'\np6\ns."

La fonction loads() dé-sérialise une chaîne vers un objet.

>>> foo3 = pickle.loads(data)
>>> foo3
{'a': 'aaa', 'c': 'ccc', 'b': 'bbb'}

DBM + Pickle = Shelves

Le module shelves exploite les deux modules précédents (en offrant l’interface du second):

  • pickle pour sérialiser les objets (éventuellement complexes),
  • anydbm pour la gestion des clés et des fichiers.
>>> import shelve

L’exemple suivant crée un fichier base dans lequel nous stockons un dictionnaire associé à la clé foo. Puis, nous ouvrons de nouveau ce fichier pour récupérer dans le dictionnaire stocké l’entrée associée à la clé a.

>>> base = shelve.open('base')
>>> base['foo'] = {'a': ['a1','a2'], 'b': ['b1','b2']}
>>> base.close()
>>> base2 = shelve.open('base')
>>> print base2['foo']['a']
['a1', 'a2']
>>> base2.close()

Remarques

  • La concurrence de mise à jour n’est pas supportée avec shelve, une possibilité est d’utiliser fcntl.
  • La définition des classes doit être importable au moment du chargement des données par pickle. Il n’est donc pas suffisant de partager le fichier de donnée, mais il faut aussi partager les modules de mise en oeuvre des classes utilisées (qui doivent être présentes dans le PYTHONPATH).
  • La compatibilité des fichiers n’est pas garantie entre deux implémentations de DBM. Il n’y a donc que portabilité du code, pas des données.

Python et SQL

Exemple avec SQlite

Python inclut un module d’accès aux bases de données offrant une interface standardisée (de facto). Des mises en oeuvre existent pour les différentes bases de données courantes. Toutefois, l’utilisation de chaque base peut varier, donc le code Python d’accès à une base n’est pas 100% portable. Il est donc recommandé dans une application de définir une couche d’abstraction de la base.

L’utilisation d’une base de données est ici illustré avec une base sqlite3.

  • Une base sqlite3 est représentée sur le disque par un fichier.
  • C’est une base embarquée (il n’y a pas de serveur à faire tourner).
  • Elle gère bien des table de plusieurs centaines de milliers de lignes.
  • Elle est incluse dans toutes les distributions de Python depuis la version 2.5.
>>> import sqlite3

Opérations de base

La fonction connect() crée un objet représentant la connexion à une base de données. Si la base n’existe pas, elle sera créé.

>>> connexion = sqlite3.connect('test.sqlite3')

Il est aussi possible de créer une base en mémoire (et donc sans version persistante sur le disque).

>>> connexion = sqlite3.connect(':memory:')

Des curseurs sont utilisés pour les interactions avec la base: émission de requêtes SQL. Ils sont créés par l’objet connexion. Un curseur offre la méthode execute qui permet de demander à la base l’évaluation d’une requête SQL. Le curseur est ici utilisé pour créer une table test1 ayant un seul champ val de type entier. Ensuite, le curseur est utilisé pour insérer une valeur dans cette table.

>>> curseur = connexion.cursor()

>>> curseur.execute("CREATE TABLE test1 (val integer)")
<sqlite3.Cursor object at 0x...>

>>> curseur.execute("INSERT INTO test1 VALUES(-1)")
<sqlite3.Cursor object at 0x...>

Insérer des données

Une insertion peut se faire en utilisant un tuple (comme dans l’exemple précédent) ou un dictionnaire pour fournir les valeurs. Dans le cas de l’utilisation des tuples, les données sont prises dans l’ordre du tuple qui doit être de même longueur que le tuple de format de la requête (argument de la clause VALUES). L’exemple suivant crée une nouvelle table et utilise un dictionnaire pour fournir les valeurs à insérer. La méthode executemany permet d’insérer une séquence de données dans une table sans avoir à faire une boucle en Python.

>>> curseur = connexion.cursor()
>>> curseur.execute('CREATE TABLE test2 (name text, firstname text)')
<sqlite3.Cursor object at 0x...>

>>> curseur.execute('INSERT INTO test2 VALUES (?, ?)', ('doe', 'john'))
<sqlite3.Cursor object at 0x...>

>>> valeurs = (('martin', 'pierre'), ('dupont', 'paul'))
>>> curseur.executemany('INSERT INTO test2 VALUES (?, ?)', valeurs)
<sqlite3.Cursor object at 0x...>

Récupérer des données

Les résultats de requêtes (réponses à une command select) sont des structures de données Python: listes (ensemble des réponses) de tuples (les données d’une réponse). Les opération fetchone() et fetchall() offertes par le curseur permettent de récupérer respectivement une valeur (en fait les valeurs une par une) ou toutes les valeurs. Attention sur les select retournant beaucoup de réponses, le fetchall() a une borne supérieure. L’exemple suivant exécute une requête sur la base et récupère d’abord la première réponse, en affichant le premier champs, puis toutes les réponses restantes, en affichant les deux champs.

>>> curseur.execute("SELECT * FROM test2")
<sqlite3.Cursor object at 0x...>

>>> print curseur.fetchone()
(u'doe', u'john')

>>> valeurs = curseur.fetchall()
>>> for v in valeurs:
...     print v[0], v[1]
...
martin pierre
dupont paul

Opérations complémentaires

L’objet représentant une connexion offre les méthodes suivantes.

  • close() demande explicitement la fermeture d’une connexion (c’est implicite lorsque l’objet connexion est détruit au sens Python).
  • commit() valide une transaction avec la base.
  • rollback() annule une transaction avec la base.

En complément, [2] présente le module dans son ensemble.

Exercices

Module «Modèle» du MVC

Ecrire un script Python qui crée une table représentant des étudiants avec les champs suivants (le type des données est précisé entre parenthèses): num dossier (integer), nom (text), prénom (text), université (text), discipline (text), niveau (integer), moyenne(integer).

Ecrire un programme Python chargeant l’ensemble des données du fichier etudiants.xml dans cette table. Pour cela, une partie du code de l’exercice précédent est réutilisable pour le parcours de l’arbre DOM.

Module «Contrôleur» du MVC

Développer une classe Controleur qui propose les traitements suivants sur la base de données:

  • obtenir la liste des numéros de dossiers, nom et prénom des étudiants,
  • obtenir la fiche complète d’un étudiant par rapport à son numéro de dossier,
  • insérer une fiche d’étudiant en fournissant toutes les informations.
[1]L’utilisation du module dbm revient à utiliser une mise en oeuvre particulière.
[2]http://docs.python.org/library/sqlite3.html