14. Fichiers▲
Ce chapitre introduit l'idée de programmes « persistants » qui gardent les données dans la mémoire permanente, et montre comment utiliser de différents types de stockage permanent, comme les fichiers et les bases de données.
14-1. Persistance▲
La majorité des programmes que nous avons vu jusqu'à présent sont transitoires dans le sens où ils s'exécutent pour un court laps de temps et affichent quelque chose, mais quand ils finissent, leurs données disparaissent. Si vous exécutez le programme à nouveau, il reprend à zéro.
D'autres programmes sont persistants : ils s'exécutent longtemps (ou tout le temps) ; ils gardent au moins une partie de leurs données en stockage permanent (un disque dur, par exemple) et s'ils s'arrêtent et redémarrent, ils reprennent d'où ils s'étaient arrêtés.
Des exemples de programmes persistants sont les systèmes d'exploitation, qui s'exécutent à peu près chaque fois qu'un ordinateur est mis en route, et les serveurs Web, qui s'exécutent en permanence, en attendant que des demandes arrivent sur le réseau.
Une des façons les plus simples de conserver les données des programmes est par la lecture et l'écriture de fichiers texte. Nous avons déjà vu des programmes qui lisent des fichiers texte ; dans ce chapitre, nous allons voir des programmes qui les écrivent.
Un autre cas classique consiste à stocker l'état du programme dans une base de données. Dans ce chapitre, je vais présenter une base de données simple et un module, pickle, qui facilite le stockage des données du programme.
14-2. Lecture et écriture▲
Un fichier texte est une séquence de caractères stockée sur un support permanent comme un disque dur, une mémoire flash ou un CD-ROM. Nous avons vu comment ouvrir et lire un fichier dans la section 9.1Lire des listes de mots.
Pour écrire un fichier, vous devez l'ouvrir avec le mode 'w' comme second paramètre :
>>>
fout =
open(
'sortie.txt'
, 'w'
)
Si le fichier existe déjà, l'ouverture en mode d'écriture efface les anciennes données et reprend à vide, donc soyez prudent ! Si le fichier n'existe pas, un nouveau fichier est créé.
open renvoie un objet fichier qui fournit des méthodes pour travailler avec le fichier. La méthode write écrit des données dans le fichier.
>>>
line1 =
"Voici l'acacia,
\n
"
>>>
fout.write
(
line1)
16
La valeur de retour est le nombre de caractères qui ont été écrits. L'objet fichier garde la trace de l'endroit où il est arrivé, donc si vous appelez la méthode write à nouveau, elle ajoute les nouvelles données à la suite de ce qui a déjà été écrit, donc à la fin du fichier.
>>>
line2 =
"l'emblème de notre pays.
\n
"
>>>
fout.write
(
line2)
25
Quand vous avez fini d'écrire, vous devez fermer le fichier.
>>>
fout.close
(
)
Si vous ne fermez pas le fichier, il sera fermé à la fin du programme.
14-3. L'opérateur de formatage▲
L'argument de write doit être une chaîne de caractères, donc si nous voulons mettre d'autres valeurs que des caractères dans un fichier, nous devons les convertir en chaînes de caractères. La façon la plus simple de le faire est en utilisant str :
>>>
x =
52
>>>
fout.write
(
str(
x))
Une autre possibilité consiste à utiliser l'opérateur de formatage, %. Lorsqu'il est appliqué à des nombres entiers, % est l'opérateur modulo (reste de la division entière). Mais lorsque le premier opérande est une chaîne, % est l'opérateur de formatage.
Le premier opérande est la chaîne de formatage, qui contient une ou plusieurs séquences de formatage, qui précisent comment est formaté le deuxième opérande. Le résultat est une chaîne.
Par exemple, la séquence de formatage '%d' signifie que le second opérande doit être formaté en tant que nombre entier en base 10 :
>>>
chameaux =
42
>>>
'
%d
'
%
chameaux
'42'
Le résultat est la chaîne '42', qui ne doit pas être confondue avec la valeur entière 42.
Une séquence de formatage peut apparaître n'importe où dans la chaîne, donc vous pouvez intercaler une valeur dans une phrase :
>>>
"J'ai repéré
%d
chameaux."
%
chameaux
"J'ai repéré 42 chameaux."
S'il y a plus d'une séquence de formatage dans la chaîne de caractères, le second argument doit être un tuple. Chaque séquence de formatage correspond à un élément du tuple, dans l'ordre.
L'exemple suivant utilise '%d' pour formater un nombre entier, '%g' pour formater un nombre à virgule flottante et '%s' pour formater une chaîne de caractères :
>>>
"En
%d
ans j'ai repéré
%g
%s
."
%
(
3
, 0.1
, 'chameaux'
)
"En 3 ans j'ai repéré 0.1 chameaux."
Le nombre d'éléments dans le tuple doit correspondre au nombre de séquences de formatage dans la chaîne de caractères. En outre, les types des éléments doivent correspondre aux séquences de formatage :
>>>
'
%d
%d
%d
'
%
(
1
, 2
)
TypeError
: not
enough arguments for
format string
>>>
'
%d
'
%
'dollars'
TypeError
: %
d format: a number is
required, not
str
Dans le premier exemple, il n'y a pas suffisamment d'éléments ; dans le second, l'élément n'a pas le bon type.
Pour plus d'informations sur l'opérateur de formatage, consultez https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting. Un autre choix, plus puissant, est la méthode format de formatage de chaîne de caractères, au sujet de laquelle vous pouvez lire sur https://docs.python.org/3/library/stdtypes.html#str.format.
14-4. Noms de fichiers et chemins▲
Les fichiers sont organisés en répertoires (également appelés « dossiers »). Chaque programme en cours d'exécution a un « répertoire courant », qui est le répertoire par défaut pour la majorité des opérations. Par exemple, lorsque vous ouvrez un fichier en lecture, Python le recherche dans le répertoire courant.
Le module os fournit des fonctions pour travailler avec des fichiers et des répertoires (« os » signifie « système d'exploitation »). os.getcwd renvoie le nom du répertoire courant :
>>>
import
os
>>>
chemin =
os.getcwd
(
)
>>>
chemin
'/home/dinsdale'
cwd signifie « répertoire de travail courant ». Dans cet exemple, le résultat est /home/dinsdale, qui est le répertoire personnel d'un d'utilisateur nommé dinsdale.
Une chaîne de caractères comme '/home/dinsdale' qui identifie un fichier ou un répertoire s'appelle un chemin.
Un simple nom de fichier, comme memo.txt est également considéré comme un chemin, mais il s'agit d'un chemin relatif, car il est relatif au répertoire courant. Si le répertoire courant est /home/dinsdale, le nom de fichier memo.txt se référerait à /home/dinsdale/memo.txt.
Un chemin qui commence par / ne dépend pas du répertoire courant ; c'est un chemin absolu. Pour trouver le chemin absolu d'un fichier, vous pouvez utiliser os.path.abspath :
>>>
os.path.abspath
(
'memo.txt'
)
'/home/dinsdale/memo.txt'
os.path offre d'autres fonctions pour travailler avec les noms et les chemins de fichiers. Par exemple, os.path.exists vérifie si un fichier ou un répertoire existe :
>>>
os.path.exists
(
'memo.txt'
)
True
S'il existe, os.path.isdir vérifie s'il est un répertoire :
>>>
os.path.isdir
(
'memo.txt'
)
False
>>>
os.path.isdir
(
'/home/dinsdale'
)
True
De façon similaire, os.path.isfile vérifie si c'est un fichier.
os.listdir renvoie une liste des fichiers (et de sous-répertoires) du répertoire donné :
>>>
os.listdir
(
chemin)
['musique'
, 'photos'
, 'memo.txt'
]
Pour illustrer ces fonctions, l'exemple suivant « parcourt » un répertoire, affiche les noms de tous les fichiers, et s'appelle lui-même de manière récursive sur tous les sous-répertoires.
def
parcourir
(
nom_repertoire):
for
nom in
os.listdir
(
nom_repertoire):
chemin =
os.path.join
(
nom_repertoire, nom)
if
os.path.isfile
(
chemin):
print
(
chemin)
else
:
parcourir
(
chemin)
os.path.join prend un répertoire et un nom de fichier et les concatène pour former un chemin d'accès complet.
Le module os fournit une fonction appelée walk qui est similaire à celle-ci, mais plus polyvalente. À titre d'exercice, lisez la documentation de cette fonction et utilisez-la pour afficher les noms des fichiers d'un répertoire donné et de ses sous-répertoires. Vous pouvez télécharger ma solution à l'adresse walk.py.
14-5. Intercepter les exceptions▲
Beaucoup de choses peuvent mal se passer lorsque vous essayez de lire et écrire des fichiers. Si vous essayez d'ouvrir un fichier qui n'existe pas, vous obtenez une IOError :
>>>
fin =
open(
'mauvais_fichier'
)
IOError
: [Errno 2
] No such file or
directory: 'mauvais_fichier'
Si vous n'êtes pas autorisé à accéder à un fichier :
>>>
fout =
open(
'/etc/passwd'
, 'w'
)
PermissionError: [Errno 13
] Permission denied: '/etc/passwd'
Et si vous essayez d'ouvrir un répertoire pour la lecture, vous obtenez
>>>
fin =
open(
'/home'
)
IsADirectoryError: [Errno 21
] Is a directory: '/home'
Pour éviter ces erreurs, vous pouvez utiliser des fonctions comme os.path.exists et os.path.isfile, mais la vérification de toutes les possibilités nécessiterait beaucoup de temps et de code (si le code d'erreur « Errno 21 » constitue un indice fiable, il existe au moins 21 choses qui peuvent mal se passer).
Il est préférable d'aller de l'avant et d'essayer - et de faire face aux problèmes s'ils se produisent - ce qui est exactement ce que fait l'instruction try (« essaie »). La syntaxe ressemble à une déclaration if...else :
try
:
fin =
open(
'mauvais_fichier'
)
except
:
print
(
"Quelque chose s'est mal passé."
)
Python commence par exécuter la clause try. Si tout se passe bien, il ignore la clause except et poursuit. Si une exception se produit, il sort de la clause try et exécute la clause except.
La gestion d'une exception par une instruction try s'appelle intercepter une exception. Dans cet exemple, la clause except affiche un message d'erreur qui n'est pas très utile. En général, intercepter une exception vous donne une chance de résoudre le problème, ou de réessayer, ou au moins de terminer le programme avec élégance.
14-6. Bases de données▲
Une base de données est un fichier (ou parfois un groupe de fichiers) qui est organisé pour stocker des données. Beaucoup de bases de données sont organisées comme un dictionnaire en ce sens qu'elles établissent une correspondance entre des clés et des valeurs. La principale différence entre une base de données et un dictionnaire est que la base de données est sur le disque (ou un autre support de stockage permanent), si bien qu'elle continue à exister après la fin de l'exécution du programme.
Le module dbm fournit une interface pour créer et mettre à jour des fichiers de base de données. À titre d'exemple, je vais créer une base de données qui contient des légendes pour fichiers image.
L'ouverture d'une base de données ressemble à l'ouverture d'autres fichiers :
>>>
import
dbm
>>>
db =
dbm.open(
'legendes'
, 'c'
)
Le mode 'c' signifie que la base de données doit être créée à vide si elle n'existe pas déjà. Le résultat est un objet base de données qui peut être utilisé (pour la majorité des opérations) comme un dictionnaire.
Lorsque vous créez un nouvel élément, dbm met à jour le fichier de base de données.
>>>
db['cleese.png'
] =
'Photo de John Cleese.'
Lorsque vous accédez à l'un des éléments, dbm lit le fichier :
>>>
db['cleese.png'
]
b'Photo de John Cleese.'
Le résultat est un objet octets (byte object), c'est pour cela qu'il commence par b. Un objet octets ressemble à une chaîne de caractères à bien des égards. Lorsque vous avancerez dans l'étude de Python, vous verrez qu'il existe des différences importantes, mais pour l'instant nous pouvons l'ignorer.
Si vous affectez une autre valeur à une clé existante, dbm remplace l'ancienne valeur :
>>>
db['cleese.png'
] =
'Photo de John Cleese marchant drôlement.'
>>>
db['cleese.png'
]
b'Photo de John Cleese marchant drôlement.'
Certaines méthodes de dictionnaire, comme keys et items, ne fonctionnent pas avec des objets base de données. Mais l'itération dans une boucle for fonctionne :
for
clef in
db:
print
(
clef, db[clef])
Comme dans le cas d'autres fichiers, vous devez fermer la base de données lorsque vous avez terminé :
>>>
db.close
(
)
14-7. Sérialiser les données avec pickle▲
Une limitation de dbm est que les clés et les valeurs doivent être des chaînes de caractères ou des octets. Si vous essayez d'utiliser un autre type, vous obtenez une erreur.
Le module pickle peut vous aider. Il traduit presque tout type d'objet en une chaîne de caractères appropriée pour le stockage dans une base de données, puis peut retraduire les chaînes de caractères en objets.
pickle.dumps prend en entrée un objet et renvoie sa représentation en chaîne de caractères (dumps est un raccourci de dump string - « copier sous forme de chaîne ») :
>>>
import
pickle
>>>
t =
[1
, 2
, 3
]
>>>
pickle.dumps
(
t)
b'
\x80\x03
]q
\x00
(K
\x01
K
\x02
K
\x03
e.'
Le format n'est pas compréhensible pour les lecteurs humains ; il est conçu pour être facile à interpréter pour pickle. pickle.loads (load string - « charger la chaîne ») reconstitue l'objet :
>>>
t1 =
[1
, 2
, 3
]
>>>
s =
pickle.dumps
(
t1)
>>>
t2 =
pickle.loads
(
s)
>>>
t2
[1
, 2
, 3
]
Même si le nouvel objet a la même valeur que l'ancien, ce n'est pas (en général) le même objet :
>>>
t1 ==
t2
True
>>>
t1 is
t2
False
Autrement dit, utiliser pickle pour sérialiser puis désérialiser un objet a le même effet que copier l'objet.
Vous pouvez utiliser pickle pour stocker dans une base de données des données qui ne sont pas des chaînes de caractères. En fait, cette combinaison est si commune qu'elle a été encapsulée dans un module appelé shelve.
14-8. Pipes▲
Les systèmes d'exploitation fournissent pour la plupart une interface de ligne de commande, souvent appelée shell. Un shell fournit habituellement des commandes pour naviguer dans le système de fichiers et lancer des applications. Par exemple, sous Unix ou Linux, vous pouvez changer de répertoire avec cd, afficher le contenu d'un répertoire avec ls, et lancer un navigateur Web en tapant (par exemple) firefox.
Tout programme que vous pouvez lancer à partir du shell peut être lancé également à partir de Python en utilisant un objet pipe, qui représente un programme en cours d'exécution.
Par exemple, la commande Unix ls -l affiche généralement le contenu du répertoire courant en format long (détaillé). Vous pouvez lancer ls avec la commande os.popen(1) :
>>>
cmd =
'ls -l'
>>>
fp =
os.popen
(
cmd)
L'argument est une chaîne de caractères qui contient une commande shell. La valeur de retour est un objet qui se comporte comme un fichier ouvert. Vous pouvez lire la sortie du processus ls ligne par ligne avec readline ou en obtenir tout le contenu avec read :
>>>
res =
fp.read
(
)
Lorsque vous aurez terminé, vous fermez le pipe comme un fichier :
>>>
stat =
fp.close
(
)
>>>
print
(
stat)
None
La valeur de retour est le statut final du processus ls ; None signifie qu'il s'est terminé normalement (sans erreur).
Par exemple, la plupart des systèmes Unix fournissent une commande appelée md5sum qui lit le contenu d'un fichier et calcule une « somme de contrôle » ou checksum. Vous trouverez des informations complémentaires au sujet de MD5 sur https://fr.wikipedia.org/wiki/MD5. Cette commande fournit un moyen efficace pour vérifier si deux fichiers ont le même contenu. La probabilité que des contenus différents donnent la même somme de contrôle est très faible (autrement dit, il y a peu de chances que cela se produise avant que l'univers s'effondre).
Vous pouvez utiliser un pipe pour exécuter md5sum à partir d'un programme Python et en obtenir le résultat :
>>>
nomfichier =
'livre.tex'
>>>
cmd =
'md5sum '
+
nomfichier
>>>
fp =
os.popen
(
cmd)
>>>
res =
fp.read
(
)
>>>
stat =
fp.close
(
)
>>>
print
(
res)
1e0033
f0ed0656636de0d75144ba32e0 livre.tex
>>>
print
(
stat)
None
14-9. Écrire des modules▲
Tout fichier qui contient du code Python peut être importé en tant que module. Par exemple, supposons que vous ayez un fichier nommé wc.py (le nom, wc, pour word count, est celui d'un petit utilitaire Unix qui compte notamment le nombre de mots et de lignes d'un fichier texte) avec le code suivant :
def
compteur_lignes
(
nomfichier):
compteur =
0
for
ligne in
open(
nomfichier):
compteur +=
1
return
compteur
print
(
compteur_lignes
(
'wc.py'
))
Si vous exécutez ce programme, il lit son propre contenu et imprime le nombre de lignes du fichier, qui est 7. Vous pouvez également l'importer comme ceci :
>>>
import
wc
7
Maintenant, vous avez un objet module wc :
>>>
wc
<
module 'wc'
from
'wc.py'
>
L'objet module fournit compteur_lignes :
>>>
wc.compteur_lignes
(
'wc.py'
)
7
Voilà donc comment vous écrivez des modules en Python.
Le seul problème avec cet exemple est que, lorsque vous importez le module, il exécute le code de test de la dernière ligne. Normalement, lorsque vous importez un module, il définit de nouvelles fonctions, mais il ne les exécute pas.
Les programmes qui seront importés en tant que modules utilisent souvent la tournure suivante :
if
__name__
==
'__main__'
:
print
(
compteur_lignes
(
'wc.py'
))
__name__ est une variable interne définie lorsque le programme démarre. Si le programme s'exécute en tant que script, __name__ a la valeur '__main__' ; dans ce cas, le code de test est exécuté. Sinon, si le module est importé, le code de test est ignoré.
À titre d'exercice, tapez cet exemple dans un fichier nommé wc.py et exécutez-le comme un script. Ensuite, exécutez l'interpréteur Python et import wc. Quelle est la valeur de __name__ lorsque le module est importé ?
Attention : Si vous tentez d'importer un module qui a déjà été importé, Python ne fait rien. Il ne relit pas le fichier, même s'il a été modifié.
Si vous voulez recharger un module, vous pouvez utiliser la fonction interne reload, mais il peut y avoir de subtiles difficultés ; par conséquent, la chose la plus sûre à faire est de redémarrer l'interpréteur et d'importer à nouveau le module.
14-10. Débogage▲
Lorsque vous lisez et écrivez des fichiers, il peut arriver que vous rencontriez des problèmes dus aux caractères non imprimables. Ces erreurs peuvent être difficiles à déboguer parce que les espaces, les tabulations et les sauts de ligne sont normalement invisibles :
>>>
s =
'1 2
\t
3
\n
4'
>>>
print
(
s)
1
2
3
4
La fonction interne repr peut vous aider. Elle prend comme argument un objet quelconque et renvoie la représentation en chaîne de caractères de celui-ci. Pour les chaînes de caractères, elle représente les caractères non imprimables précédés par des barres obliques inverses :
>>>
print
(
repr(
s))
'1 2
\t
3
\n
4'
Cela peut être utile pour le débogage.
Un autre problème que vous pourriez rencontrer est que les différents systèmes d'exploitation utilisent différents caractères pour indiquer la fin d'une ligne. Certains systèmes, comme Unix ou Linux, utilisent un saut de ligne, représenté comme \n. D'autres utilisent un caractère de retour chariot, représenté comme \r. Certains, comme Windows, utilisent les deux. Si vous déplacez des fichiers entre les différents systèmes, ces incohérences peuvent causer des problèmes.
Pour la majorité des systèmes, il existe des applications pour conversion d'un format à un autre. Vous pouvez les trouver (et en savoir plus à ce sujet) à l'adresse https://fr.wikipedia.org/wiki/Fin_de_ligne. Ou, bien sûr, vous pouvez en écrire une vous-même.
14-11. Glossaire▲
- persistant : se rapportant à un programme qui fonctionne indéfiniment ou maintient au moins une partie de ses données dans un support de stockage permanent.
- opérateur de formatage : un opérateur, %, qui prend une chaîne de formatage et un tuple et génère une chaîne qui contient les éléments du tuple formatés comme spécifié par la chaîne de formatage.
- chaîne de formatage : une chaîne, utilisée avec l'opérateur de formatage, qui contient des séquences de formatage.
- séquence de formatage : une séquence de caractères dans une chaîne de formatage, comme %d, qui spécifie comment une valeur doit être formatée.
- fichier texte : une séquence de caractères stockée dans un support permanent tel qu'un disque dur.
- répertoire : une collection de fichiers nommée, parfois aussi appelée un dossier.
- chemin : une chaîne de caractères qui identifie un fichier.
- chemin relatif : un chemin qui commence à partir du répertoire courant.
- chemin absolu : un chemin qui commence à partir du répertoire le plus élevé dans le système de fichiers.
- intercepter : éviter qu'une exception mette fin à un programme, en utilisant les instructions try et except.
- base de données : un fichier dont le contenu est organisé comme un dictionnaire avec des clés qui correspondent à des valeurs.
- objet octets : un objet similaire à une chaîne.
- shell : un programme qui permet aux utilisateurs de taper des commandes et les exécuter pour lancer d'autres programmes.
- objet pipe : un objet qui représente un programme en cours d'exécution, permettant à un programme Python d'exécuter des commandes et d'en lire les résultats.
14-12. Exercices▲
Exercice 1
Écrivez une fonction appelée sed qui prend pour arguments un motif en tant que chaîne de caractères, une chaîne de caractères de remplacement et deux noms de fichiers ; elle devra lire le premier fichier et écrire son contenu dans le second fichier (en le créant si nécessaire). Si la chaîne de motif apparaît à un endroit quelconque dans le fichier, elle devra être remplacée par la chaîne de remplacement.
Si une erreur se produit lors de l'ouverture, la lecture, l'écriture ou de fermeture des fichiers, votre programme doit intercepter l'exception, afficher un message d'erreur et se terminer. Solution : sed.py.
Exercice 2
Si vous téléchargez ma solution à l'exercice 2 du chapitre 12 à l'adresse anagram_sets.py , vous verrez qu'elle crée un dictionnaire qui établit une correspondance entre une chaîne de caractères triée et une liste des mots qui peuvent être orthographiés avec ces lettres. Par exemple, 'AEILNPT' mappe vers la liste ['PLATINE', 'PATELIN', 'PLAINTE', 'EPILANT', 'PLIANTE'] .
Écrivez un module qui importe anagram_sets et fournit deux nouvelles fonctions : store_anagrams devra stocker le dictionnaire d'anagrammes dans une « étagère » shelve ; read_anagrams devra rechercher un mot et renvoyer une liste de ses anagrammes. Solution : anagram_db.py.
Dans une grande collection de fichiers MP3, il peut y avoir plusieurs copies de la même chanson, stockées dans des répertoires différents ou avec des noms de fichiers différents. Le but de cet exercice est de rechercher les doublons.
- Écrivez un programme qui recherche dans un répertoire et tous ses sous-répertoires, de manière récursive, et renvoie une liste de chemins complets pour tous les fichiers avec un suffixe donné (comme .mp3 ). Indice : os.path offre plusieurs fonctions utiles pour la manipulation des noms de fichier et de chemin.
- Pour reconnaître les doublons, vous pouvez utiliser md5sum pour calculer une somme de contrôle pour chaque fichier. Si deux fichiers ont la même somme de contrôle, elles ont probablement le même contenu.
- Pour une vérification supplémentaire, vous pouvez utiliser la commande Unix diff .
Solution : find_duplicates.py.