Objectifs du chapitre
À la fin de ce chapitre, vous serez capable de:
- ouvrir, lire et écrire un fichier texte avec
openet le blocwith, en choisissant le bon mode et en sachant ce que chacun détruit; - lire un fichier ligne par ligne, nettoyer chaque ligne avec
strip()et convertir ses champs en nombres; - lire et produire un fichier CSV à la suisse, au point-virgule et à la virgule décimale, à la main puis avec le module
csv; - intercepter une erreur avec
try/except, en nommant l'exception attendue, et dire ce que fontelseetfinally; - lever vous-même une exception avec
raisepour signaler une donnée invalide, et lire la trace d'erreur qui en résulte; - répartir un programme en modules, l'importer, et protéger son code de démonstration avec
if __name__ == "__main__":.
Un programme dont les données meurent avec lui
Tous les programmes des sept premiers chapitres ont un défaut commun: quand ils s'arrêtent, tout disparaît. Le carnet de notes du chapitre 7 est reconstruit à chaque exécution parce que les dix notes sont écrites dans le code lui-même. Si votre assistant vous transmet les notes du semestre suivant, il faut modifier le programme. Si vous voulez traiter mille relevés au lieu de dix, il faut coller mille lignes dans le fichier source. Et si le programme calcule pendant une heure, le résultat s'évapore avec la fenêtre du terminal.
Un programme utile fait donc trois choses dans cet ordre: il lit des données qu'il n'a pas produites, il les traite, il écrit un résultat qui lui survit. C'est le cycle lire → traiter → écrire, et il sépare proprement trois responsabilités: les données vivent dans un fichier, la logique vit dans le programme, et le résultat repart dans un autre fichier. Le même programme sert alors pour dix relevés ou pour mille, sans qu'une seule ligne de code change.
Un fichier (file) est une suite d'octets conservée par le système d'exploitation sous un nom. Nous ne traiterons dans ce chapitre que des fichiers texte: des fichiers dont les octets se lisent comme des caractères, organisés en lignes séparées par un caractère spécial, le retour à la ligne (newline), que Python écrit \n. Un fichier .txt, un fichier .csv, un fichier .py sont des fichiers texte; une image .png ou un classeur .xlsx ne le sont pas, et s'ouvrent avec d'autres outils.
Ouvrir un fichier
Avant de lire ou d'écrire, il faut ouvrir le fichier. La fonction native open prend le nom du fichier et un mode, et renvoie un objet fichier par lequel passeront toutes les opérations suivantes.
Les trois modes qui suffisent à ce cours:
| Mode | Nom | Le fichier n'existe pas | Le fichier existe |
|---|---|---|---|
"r" | lecture (read) | erreur FileNotFoundError | il est lu depuis le début |
"w" | écriture (write) | il est créé, vide | son contenu est effacé |
"a" | ajout (append) | il est créé, vide | on écrit à la fin, rien n'est perdu |
Le mode est facultatif: open("notes.txt") équivaut à open("notes.txt", "r"). Écrivez-le quand même. Un mode explicite se relit, et le jour où vous passerez de "r" à "w" en modifiant un programme, vous verrez ce que vous faites.
Voici la version longue, celle qu'il faut avoir vue une fois pour comprendre la suivante:
# ouverture, écriture, fermeture: la version longue
fichier = open("essai.txt", "w")
fichier.write("Bonjour\n")
fichier.close()
print("fichier ferme:", fichier.closed)
fichier ferme: True
Trois opérations distinctes: on ouvre, on écrit, on ferme. La fermeture n'est pas une politesse. Tant que le fichier est ouvert, le système peut garder ce que vous avez écrit dans une mémoire tampon sans l'avoir encore posé sur le disque; close() garantit que tout y est réellement. Un programme qui oublie close() peut donc produire un fichier tronqué ou vide, et ce genre de bogue est particulièrement désagréable parce qu'il ne se manifeste pas toujours: sur un petit fichier, tout est écrit quand même; sur un gros, il manque la fin.
Il y a pire. Si une erreur survient entre l'ouverture et la fermeture, le programme s'arrête avant d'atteindre close(), et le fichier reste ouvert. Un programme qui ouvre des fichiers dans une boucle sans les fermer finit par épuiser le quota de fichiers ouverts que le système lui accorde.
Le bloc with, et ce qu'il garantit
La solution tient en un mot-clé:
with open("essai.txt", "w") as fichier:
fichier.write("Bonjour\n")
# ici, hors du bloc, le fichier est déjà fermé
print("fichier ferme:", fichier.closed)
fichier ferme: True
La ligne with open(...) as fichier: ouvre le fichier, met l'objet obtenu dans la variable fichier, et exécute le bloc indenté. Quoi qu'il arrive ensuite, le fichier est fermé à la sortie du bloc: à la fin normale, sur un return, et aussi lorsqu'une erreur interrompt le bloc au milieu. C'est cette dernière garantie qui rend with indispensable.
try:
with open("essai.txt", "w") as fichier:
fichier.write("debut\n")
resultat = 1 / 0 # une erreur au milieu du bloc
except ZeroDivisionError:
print("erreur interceptee")
print("fichier ferme:", fichier.closed)
with open("essai.txt", "r") as relecture:
erreur interceptee
fichier ferme: True
contenu ecrit: 'debut\n'
Le try / except de cet exemple est expliqué plus loin dans ce chapitre; retenez pour l'instant le résultat: la division par zéro a interrompu le bloc, et le fichier a tout de même été fermé et son contenu écrit. Sans with, la ligne close() aurait été sautée.
Retenez donc la règle, sans chercher pour l'instant comment with s'y prend: on n'ouvre pas un fichier autrement qu'avec with. Le mécanisme général derrière ce mot-clé — le protocole des gestionnaires de contexte — n'a aucune importance ici; ce qui compte est la garantie.
Vous voulez ajouter une ligne à la fin d'un fichier de mesures qui contient déjà trois ans de relevés. Quel mode utilisez-vous?
Lire un fichier
Créons d'abord le fichier qui servira de fil rouge: les sept températures moyennes journalières de la semaine, une par ligne. Ces valeurs sont fictives et ne proviennent d'aucune station de mesure réelle.
# crée le fichier de données utilisé dans toute la suite
temperatures = [2.4, 5.1, 7.8, 6.2, 3.9, -0.5, 1.7]
with open("temperatures.txt", "w") as fichier:
for valeur in temperatures:
fichier.write(str(valeur) + "\n")
Le programme n'affiche rien: son effet est sur le disque. Ouvrez temperatures.txt dans un éditeur de texte, vous y verrez sept lignes.
Tout le fichier d'un coup
La méthode read() renvoie tout le contenu du fichier dans une seule chaîne de caractères.
with open("temperatures.txt", "r") as fichier:
contenu = fichier.read()
print(type(contenu))
print(len(contenu))
print(contenu)
<class 'str'>
29
2.4
5.1
7.8
6.2
3.9
-0.5
1.7
Trois observations. D'abord, read() rend une chaîne, pas une liste: c'est du texte brut, et les sept valeurs ne sont pas encore sept nombres. Ensuite, cette chaîne fait 29 caractères: les 22 caractères des sept nombres, plus les sept \n. Enfin, l'affichage se termine par une ligne vide, parce que la chaîne se termine déjà par un \n et que print en ajoute un second.
Ligne par ligne, avec un for
Un objet fichier ouvert en lecture est itérable: on peut le parcourir avec une boucle for, exactement comme une liste, et chaque tour donne une ligne.
with open("temperatures.txt", "r") as fichier:
for ligne in fichier:
print(repr(ligne))
'2.4\n'
'5.1\n'
'7.8\n'
'6.2\n'
'3.9\n'
'-0.5\n'
'1.7\n'
Nous avons utilisé repr() plutôt que print(ligne) pour voir ce qu'il y a vraiment dans la variable: la chaîne '2.4\n' et non '2.4'. Le retour à la ligne fait partie de la ligne lue. C'est la source d'erreur numéro un des débutants avec les fichiers.
Pourquoi préférer cette boucle à read()? Parce qu'elle ne charge qu'une ligne à la fois en mémoire. Un fichier de relevés météo d'un million de lignes pèse une trentaine de mégaoctets: read() en fait une chaîne de trente millions de caractères, quand la boucle for n'occupe jamais que la place d'une ligne. Sur un fichier assez gros, read() fait tomber le programme avec une MemoryError, alors que la boucle s'exécute sans difficulté. La lecture ligne par ligne est donc le comportement par défaut, et read() un raccourci réservé aux petits fichiers.
strip(), presque toujours
La méthode strip() du chapitre 5 enlève les espaces et les retours à la ligne au début et à la fin d'une chaîne. Sur une ligne lue dans un fichier, elle enlève le \n final.
with open("temperatures.txt", "r") as fichier:
premiere = fichier.readline()
print(repr(premiere))
print(premiere == "2.4")
print(premiere.strip() == "2.4")
'2.4\n'
False
True
La méthode readline() lit une ligne et avance la position; nous l'utilisons ici pour n'en examiner qu'une. Et la comparaison premiere == "2.4" est fausse: les deux chaînes diffèrent d'un caractère invisible. Un programme qui cherche la ligne "fin" dans un fichier et compare sans strip() ne la trouvera jamais, et vous chercherez longtemps pourquoi, parce que l'affichage à l'écran est identique dans les deux cas.
Un mot d'honnêteté: float("2.4\n") fonctionne, car float et int tolèrent les espaces et les retours à la ligne autour du nombre. Mais cette tolérance est une exception, pas une règle: dès que la ligne contient plusieurs champs, dès qu'on la compare, dès qu'on la découpe ou qu'on l'affiche, le \n gêne. Prenez l'habitude de nettoyer systématiquement: ligne.strip() d'abord, tout le reste ensuite.
Le fichier temperatures.txt contient les sept valeurs, une par ligne, chacune suivie d'un retour à la ligne. Combien de caractères la méthode read() renvoie-t-elle au total?
Écrire dans un fichier
La méthode write() écrit une chaîne dans un fichier ouvert en écriture. Elle a une particularité qui surprend tout le monde une fois: elle n'ajoute pas de retour à la ligne. Contrairement à print, qui termine chaque appel par un \n invisible, write écrit exactement les caractères qu'on lui donne, ni plus ni moins.
with open("sansretour.txt", "w") as fichier:
fichier.write("un")
fichier.write("deux")
fichier.write("trois")
with open("sansretour.txt", "r") as fichier:
print(repr(fichier.read()))
'undeuxtrois'
Trois appels, une seule ligne dans le fichier. Pour obtenir trois lignes, il faut écrire les \n soi-même:
jours = ["lundi", "mardi", "mercredi"]
with open("jours.txt", "w") as fichier:
for jour in jours:
fichier.write(jour + "\n")
with open("jours.txt", "r") as fichier:
print(repr(fichier.read()))
'lundi\nmardi\nmercredi\n'
Ce petit désagrément a une contrepartie: write vous laisse maîtriser au caractère près ce qui part sur le disque. C'est ce qu'on veut quand on produit un fichier destiné à être relu par un autre programme, où une espace en trop change le résultat de la lecture.
Une dernière remarque: write n'accepte que des chaînes. fichier.write(3.8) produit une TypeError; il faut écrire fichier.write(str(3.8)) ou, mieux, utiliser une f-string du chapitre 5: fichier.write(f"{moyenne:.2f}\n").
Le mode "w" détruit, et ne prévient pas
Le mode "a", lui, conserve tout et écrit à la fin:
with open("journal.txt", "w") as fichier:
fichier.write("lundi: 2.4\n")
# "a" ajoute à la fin sans rien effacer
with open("journal.txt", "a") as fichier:
fichier.write("mardi: 5.1\n")
with open("journal.txt", "r") as fichier:
print(fichier.read(), end=""
lundi: 2.4
mardi: 5.1
Le end="" de print évite la ligne vide finale dont nous avons parlé plus haut. Le mode "a" est celui des journaux (logs): un programme qui tourne chaque jour et ajoute une ligne au bilan, un capteur qui enregistre une mesure par heure. Remarquez enfin qu'un fichier ouvert en "a" est créé s'il n'existe pas: on n'a pas besoin de traiter à part le premier jour.
Remettez dans l'ordre les étapes d'un programme qui produit un rapport à partir d'un fichier de mesures.
Glissez les éléments pour les mettre dans le bon ordre
- ouvrir le fichier de rapport en mode w
- fermer le fichier de mesures (le bloc with s'en charge)
- écrire les statistiques, retours à la ligne compris
- calculer la moyenne, le maximum et le minimum
- lire les lignes une à une et convertir chaque valeur en nombre
- ouvrir le fichier de mesures en mode r
Les chemins: où le programme cherche-t-il vos fichiers?
Jusqu'ici nous avons écrit open("temperatures.txt"), sans dire où se trouve ce fichier. Python le cherche dans le dossier de travail (current working directory), c'est-à-dire le dossier depuis lequel le programme a été lancé — et non le dossier qui contient le fichier .py. Les deux coïncident tant que vous lancez python3 programme.py depuis le dossier du programme; ils diffèrent dès que ce n'est plus le cas, et c'est alors que tout casse.
Prenons un programme minuscule, enregistré sous lecture.py, dans le même dossier que temperatures.txt:
with open("temperatures.txt", "r") as fichier:
print(fichier.readline().strip())
Lancé depuis ce dossier, il affiche 2.4. Copié dans un sous-dossier et lancé de là, il échoue:
Traceback (most recent call last):
File "lecture.py", line 1, in <module>
with open("temperatures.txt", "r") as fichier:
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'temperatures.txt'
Le programme n'a pas changé d'une virgule; c'est le dossier de travail qui a changé. Lisez la dernière ligne: Python vous redit exactement le chemin qu'il a essayé, 'temperatures.txt', sans dossier — preuve qu'il l'a cherché dans le dossier de travail.
Trois conseils pratiques, dans l'ordre d'importance:
- Préférez les chemins relatifs dans vos programmes, et rangez données et programme ensemble. Un chemin absolu comme
/home/marie/mesures/temperatures.txtmarche sur la machine de Marie et nulle part ailleurs; votre camarade, votre assistant et le serveur de rendu ont tous une autre arborescence. - Utilisez la barre oblique
/comme séparateur, y compris sous Windows, où Python l'accepte partout:open("donnees/temperatures.txt"). La barre inverse\de Windows, elle, entre en conflit avec les séquences d'échappement des chaînes —"donnees\temperatures.txt"contient un\t, c'est-à-dire une tabulation. - Ne devinez pas, vérifiez. Si un
FileNotFoundErrorvous surprend alors que le fichier existe «juste là», c'est que le dossier de travail n'est pas celui que vous croyez. La fonctiongetcwd()du moduleosl'affiche; nous n'en aurons pas besoin ailleurs dans ce cours, mais elle répond à cette question en une ligne.
Un fichier de données: le CSV
Un fichier texte d'une valeur par ligne convient pour une seule grandeur. Dès qu'il y a plusieurs colonnes — un jour et une température —, le format usuel est le CSV (comma-separated values): une ligne par enregistrement, des champs séparés par un caractère, et souvent une première ligne d'en-tête qui nomme les colonnes. C'est ce qu'exportent les tableurs, les bases de données et la plupart des instruments de mesure.
Créons le fichier de la semaine, avec son en-tête. Données fictives, toujours les mêmes que plus haut:
lignes = ["jour;temperature", "lundi;2,4", "mardi;5,1", "mercredi;7,8",
"jeudi;6,2", "vendredi;3,9", "samedi;-0,5", "dimanche;1,7"]
with open("semaine.csv", "w") as fichier:
for ligne in lignes:
fichier.write(ligne + "\n")
Le programme n'affiche rien; le fichier semaine.csv contient désormais:
jour;temperature
lundi;2,4
mardi;5,1
mercredi;7,8
jeudi;6,2
vendredi;3,9
samedi;-0,5
dimanche;1,7
À la main, avec split
La méthode split(";") du chapitre 5 découpe une chaîne en liste de morceaux. Avec elle et strip(), on lit déjà un CSV simple:
with open("semaine.csv", "r") as fichier:
entete = fichier.readline() # on met l'en-tête de côté
print("colonnes:", entete.strip().split(";"))
for ligne in fichier:
champs = ligne.strip().split(";")
jour = champs[0]
temperature = float(champs[1].replace(",", "."))
colonnes: ['jour', 'temperature']
lundi -> 2.4
mardi -> 5.1
mercredi -> 7.8
jeudi -> 6.2
vendredi -> 3.9
samedi -> -0.5
dimanche -> 1.7
Notez l'enchaînement ligne.strip().split(";"): on nettoie d'abord, on découpe ensuite. Dans l'autre ordre, le dernier champ garderait son \n et la conversion en nombre deviendrait fragile. Notez aussi que le premier readline() consomme l'en-tête: la boucle for qui suit reprend là où la position de lecture s'est arrêtée, donc à la deuxième ligne. C'est la position de lecture de la figure 8.1 à l'œuvre.
Avec le module csv
Cette lecture à la main a une limite: elle suppose qu'aucun champ ne contient le séparateur. Un vrai CSV autorise "Meyer, Anne";4.5, où la virgule est protégée par des guillemets, et là split se trompe. Le module csv de la bibliothèque standard connaît ces règles.
import csv
with open("semaine.csv", "r", newline="") as fichier:
lecteur = csv.reader(fichier, delimiter=";")
entete = next(lecteur)
print("en-tete:", entete)
for ligne in lecteur:
print(ligne)
en-tete: ['jour', 'temperature']
['lundi', '2,4']
['mardi', '5,1']
['mercredi', '7,8']
['jeudi', '6,2']
['vendredi', '3,9']
['samedi', '-0,5']
['dimanche', '1,7']
Quatre choses nouvelles. csv.reader(fichier, delimiter=";") fabrique un lecteur qui se parcourt avec un for, et qui rend une liste de chaînes par ligne, déjà découpée et déjà débarrassée du \n. La fonction native next(lecteur) en prend un seul élément, ce qui est la façon propre de mettre l'en-tête de côté. L'argument newline="" de open est recommandé par la documentation du module: il laisse csv gérer lui-même les fins de ligne, qui ne s'écrivent pas de la même façon sur tous les systèmes. Et les champs restent des chaînes: '2,4', pas 2.4. La conversion reste à votre charge.
On ajoute au fichier semaine.csv une huitième mesure, lundi;-1,2. Combien de jours de gel le programme de l'exemple 8.2 comptera-t-il alors?
Les exceptions
Depuis le chapitre 1, les erreurs arrêtent nos programmes. Une conversion impossible, une division par zéro, un fichier absent: Python affiche une trace et rend la main. Ce chapitre est celui où l'on cesse de subir ces erreurs pour les traiter.
Le mot est bien choisi: une exception signale une situation exceptionnelle, que le code n'a pas su gérer sur place. Ce n'est pas nécessairement un bogue: un fichier absent, une ligne abîmée, une saisie erronée d'un utilisateur sont des accidents normaux de la vie d'un programme. Un bon programme les prévoit.
Voici un exemple minuscule. Une liste de mesures reçues sous forme de texte, dont une ligne est illisible:
mesures = ["12", "7", "trois", "5"]
total = 0
for texte in mesures:
total = total + int(texte)
print(total)
Traceback (most recent call last):
File "total.py", line 4, in <module>
total = total + int(texte)
~~~^^^^^^^
ValueError: invalid literal for int() with base 10: 'trois'
Relisons cette trace ligne par ligne, comme au chapitre 1. Traceback (most recent call last) annonce la pile des appels, du plus ancien au plus récent. File "total.py", line 4 donne le fichier et la ligne fautive. La ligne suivante reproduit le code, et les ~~~^^^ soulignent la partie exacte de l'expression qui a échoué: int(texte), pas l'addition. Enfin, la dernière ligne donne le type — ValueError — et le message — invalid literal for int() with base 10: 'trois', c'est-à-dire «'trois' n'est pas une écriture valide d'un entier en base 10». Les deux comptent: le type sert à attraper l'exception, le message à comprendre.
try et except
La parade s'écrit en deux blocs. Le bloc try contient les instructions à risque; le bloc except dit quoi faire si l'une d'elles échoue.
mesures = ["12", "7", "trois", "5"]
total = 0
ignorees = 0
for texte in mesures:
try:
total = total + int(texte)
except ValueError:
ignorees = ignorees + 1
print("total:", total)
print(
total: 24
valeurs ignorees: 1
Le programme ne s'arrête plus: il additionne 12, 7 et 5, compte une valeur ignorée, et termine. Trois points de syntaxe et un point de sens.
- Le
tryet leexceptsont deux blocs indentés, comme ceux d'unif. L'indentation est de la syntaxe, ici comme ailleurs. - Le nom qui suit
exceptest le type de l'exception à attraper.except ValueError:n'attrape que lesValueError; toute autre exception continue sa route. - Placer le
tryà l'intérieur de la boucle, comme ici, fait que chaque tour est protégé séparément, et que la boucle continue après une erreur. Avec letryautour de la boucle entière, la première mauvaise valeur mettrait fin au parcours. - Une exception attrapée n'est pas une exception qui n'a pas eu lieu: l'instruction fautive n'a pas produit de valeur. Ici,
totaln'a rien reçu pour"trois". Le programme doit décider quoi faire — ignorer, compter, remplacer par une valeur par défaut, prévenir — et ce choix vous appartient.
Attraper la bonne exception
Chaque situation a son type. Les quatre que vous rencontrerez le plus:
| Exception | Cause typique |
|---|---|
FileNotFoundError | open en mode "r" sur un fichier qui n'existe pas |
ValueError | int("trois"), float("2,4"), une valeur du bon type mais impossible |
ZeroDivisionError | une division ou un modulo par zéro |
TypeError | une opération entre types incompatibles, comme "3" + 5 |
Un fichier absent, par exemple, n'est pas une raison d'arrêter un programme qui sait repartir de zéro:
try:
with open("sauvegarde.txt", "r") as fichier:
contenu = fichier.read()
except FileNotFoundError:
print("aucune sauvegarde: on repart de zero")
contenu = ""
print("contenu:", repr(contenu))
aucune sauvegarde: on repart de zero
contenu: ''
Et une division par zéro se traite là où elle se produit:
def diviser(a, b):
try:
return a / b
except ZeroDivisionError:
print(" (division par zero interceptee)")
return None
print(diviser(7, 2))
print(diviser(7, 0))
3.5
(division par zero interceptee)
None
La valeur None du chapitre 4 est ici un choix délibéré: la fonction annonce «je n'ai pas de résultat» au lieu d'inventer un zéro qui se propagerait dans les calculs suivants.
Le piège: except: tout seul
Un programme de 200 lignes affiche un résultat visiblement faux, sans aucun message d'erreur. En le relisant, vous trouvez un bloc try dont le except est écrit sans nom de type. Quelle est l'explication la plus probable?
Où va l'exception? La remontée
Une exception ne s'arrête pas à la fonction où elle est levée. Elle remonte la chaîne des appels jusqu'à trouver un try prêt à l'attraper. C'est ce qui permet de traiter les erreurs là où on sait quoi en faire, et non là où elles se produisent.
def convertir(texte):
valeur = float(texte.replace(",", "."))
return valeur
def lire_ligne(ligne):
jour, texte = ligne.split(";")
valeur = convertir(texte)
return jour, valeur
def traiter(lignes):
for ligne in lignes:
try:
print("ok :", lire_ligne(ligne))
ok : ('lundi', 2.4)
ignoree: could not convert string to float: 'n/d'
Sur la première ligne, tout va bien. Sur la seconde, float("n/d") échoue dans convertir. Cette fonction ne sait pas quoi faire d'une mesure illisible — elle ne sait même pas qu'elle travaille sur un relevé météo. Elle laisse donc l'exception remonter: lire_ligne ne l'attrape pas non plus, et son return n'est jamais exécuté; traiter, qui sait qu'une ligne abîmée doit être ignorée, l'attrape enfin.
La forme except ValueError as erreur: donne un nom à l'objet exception, ce qui permet d'afficher son message. Ici, print(erreur) affiche could not convert string to float: 'n/d' — le message de Python, en anglais, et il est bien plus précis que tout ce que vous pourriez écrire à la main.
Il faut bien mesurer ce que dit la figure 8.2: entre le point où l'exception est levée et le except qui l'attrape, tout est abandonné. Le return valeur de convertir, le return jour, valeur de lire_ligne ne sont pas exécutés; les variables locales de ces deux fonctions disparaissent. Une exception n'est pas un if déguisé: c'est une sortie d'urgence qui traverse les étages.
Le fichier releves.txt n'existe pas dans ce dossier. Faites afficher aucun releve au lieu d'interrompre le programme, et laissez la dernière ligne s'exécuter.
else et finally: les deux blocs qu'on oublie
Un try complet peut comporter quatre blocs. Voici la règle, énoncée sans détour:
try: les instructions surveillées. Exécuté toujours, mais interrompu dès qu'une exception y est levée.except X:: exécuté seulement si une exception de typeXa été levée dans letry.else:: exécuté seulement si letrys'est terminé sans aucune exception.finally:: exécuté dans tous les cas, en dernier — que letryréussisse, qu'unexceptattrape l'erreur, ou que personne ne l'attrape et que le programme s'arrête.
def lire_nombre(texte):
try:
valeur = float(texte)
except ValueError:
print(" except : conversion impossible")
return None
else:
print(" else : conversion reussie")
return valeur
finally:
print(" finally: execute dans tous les cas")
print(lire_nombre("3.8"))
print(lire_nombre("froid"
else : conversion reussie
finally: execute dans tous les cas
3.8
except : conversion impossible
finally: execute dans tous les cas
None
Regardez l'ordre de près. Dans le premier appel, le else s'exécute, puis le finally, puis seulement la valeur remonte au print de l'appelant — alors même que le return était écrit dans le else. Le finally passe avant le retour: c'est sa raison d'être. Dans le second appel, l'exception est attrapée, le else est sauté, et le finally s'exécute quand même.
À quoi servent ces deux blocs? Le else sert à garder le try court. Tout ce qui suit l'instruction risquée et qui n'est pas lui-même risqué appartient au else; l'y mettre évite d'attraper par mégarde une erreur venue d'ailleurs. Le finally sert au nettoyage obligatoire: fermer une connexion, remettre un capteur au repos, effacer un fichier temporaire. Dans ce cours, c'est précisément ce que with fait pour les fichiers — with est, si l'on veut, un finally: fichier.close() écrit une fois pour toutes.
L'explorateur ci-dessous applique ce schéma à un petit programme réel, sur quatre entrées différentes. Faites varier l'entrée et regardez quelles lignes s'allument; puis retirez deux des trois clauses except et voyez ce qui se passe quand personne n'attrape l'exception — le finally s'exécute encore, avant que le programme ne s'arrête.
Choisissez l’entrée donnée au programme, et voyez quelles lignes s’exécutent, quelle exception est levée et qui l’attrape.
Dans un bloc try / except / finally, une exception est levée dans le try et aucun except ne correspond à son type. Que se passe-t-il?
Lever une exception soi-même: raise
Jusqu'ici, les exceptions venaient de Python. Vous pouvez en lever vous-même, avec raise, et c'est ainsi qu'une fonction refuse une donnée qu'elle ne peut pas traiter honnêtement.
def verifier_temperature(valeur):
"""Valide la temperature: leve ValueError si elle est absurde."""
if valeur < -60 or valeur > 60:
raise ValueError(f"temperature invraisemblable: {valeur}")
return valeur
print(verifier_temperature(2.4))
print(verifier_temperature(120.0))
2.4
Traceback (most recent call last):
File "controle.py", line 8, in <module>
print(verifier_temperature(120.0))
~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "controle.py", line 4, in verifier_temperature
raise ValueError(f"temperature invraisemblable: {valeur}")
ValueError: temperature invraisemblable: 120.0
La syntaxe est raise Type("message"). Choisissez le type qui décrit le mieux la faute — ValueError pour une valeur du bon type mais inacceptable, TypeError pour un type incompatible — et écrivez un message qui contient la valeur fautive. Comparez: «donnée invalide» ne dit rien; temperature invraisemblable: 120.0 dit ce qui a été refusé et pourquoi, et vous fait gagner l'essentiel du temps de déverminage.
Une fonction qui lève une exception n'impose rien à son appelant: celui-ci décide.
for valeur in [2.4, 120.0, -0.5]:
try:
print("retenue :", verifier_temperature(valeur))
except ValueError as erreur:
print("rejetee :", erreur)
retenue : 2.4
rejetee : temperature invraisemblable: 120.0
retenue : -0.5
C'est la division du travail qu'apportent les exceptions, et elle mérite d'être formulée: la fonction qui détecte le problème n'est pas celle qui décide quoi en faire. verifier_temperature connaît le domaine physique plausible; elle ne sait pas si une mesure aberrante doit interrompre le traitement, être remplacée par la moyenne ou simplement être comptée. La boucle, elle, le sait. Sans exception, il faudrait renvoyer un code d'erreur et le tester à chaque appel — ce que personne ne fait avec constance.
Notez enfin qu'un raise termine la fonction, comme un return: la ligne return valeur n'est jamais atteinte quand le test échoue.
Le fichier releves.csv est déjà là, avec une ligne d'en-tête et trois relevés. Lisez-le et affichez la moyenne des températures, arrondie à un chiffre après la virgule.
Les modules
Un programme de cinq cents lignes dans un seul fichier est illisible, et ses fonctions ne servent qu'à lui. La réponse tient en un mot: le module.
Les trois formes d'importation
import math
print(math.sqrt(2))
print(math.floor(3.8), math.ceil(3.8))
print(round(math.pi, 4))
1.4142135623730951
3 4
3.1416
import math rend le module accessible sous son nom: on écrit math.sqrt. Ce préfixe est une qualité, pas une lourdeur — il dit d'où vient la fonction et évite qu'un sqrt défini par vous n'entre en collision avec celui du module.
from math import sqrt, pi
print(sqrt(2))
print(round(pi, 4))
import math as m
print(m.floor(3.8))
1.4142135623730951
3.1416
3
from math import sqrt, pi importe deux noms précis et les met à disposition sans préfixe. C'est commode quand on utilise intensément deux ou trois fonctions. Évitez en revanche from math import *, qui déverse tous les noms du module dans le vôtre: on ne sait plus qui vient d'où, et une redéfinition silencieuse devient possible. Enfin, import math as m donne un alias: utile pour les noms longs, inutile pour math.
Les quatre modules standard de ce cours:
| Module | Ce qu'on y prend |
|---|---|
math | sqrt, floor, ceil, pi, log, sin — les mathématiques usuelles |
random | random, randint, choice, shuffle — le tirage au sort |
csv | reader, writer — la lecture et l'écriture de fichiers tabulaires |
sys | argv (les arguments de la ligne de commande), exit (arrêter le programme) |
Écrire son propre module
Un module, c'est simplement un fichier. Enregistrez celui-ci sous le nom statistiques.py:
"""Quelques statistiques elementaires sur une liste de nombres."""
def moyenne(valeurs):
"""Moyenne arithmetique. Leve ValueError si la liste est vide."""
if len(valeurs) == 0:
raise ValueError("liste vide: pas de moyenne")
return sum(valeurs) / len(valeurs)
def etendue(valeurs):
"""Difference entre la plus grande et la plus petite valeur."""
if len(valeurs) == 0:
raise ValueError("liste vide: pas d'etendue")
return
Et dans un second fichier, principal.py, placé dans le même dossier:
import statistiques
temperatures = [2.4, 5.1, 7.8, 6.2, 3.9, -0.5, 1.7]
print("moyenne:", round(statistiques.moyenne(temperatures), 2))
print("etendue:", round(statistiques.etendue(temperatures), 2))
moyenne: 3.8
etendue: 8.3
Le nom du module est celui du fichier sans le .py, et Python le cherche d'abord dans le dossier du programme lancé. Deux conséquences pratiques: ne nommez jamais un de vos fichiers math.py, csv.py ou random.py — il masquerait le module standard du même nom, et les erreurs qui en résultent sont déroutantes; et gardez, au moins tant que vous débutez, vos modules dans le même dossier que le programme qui les importe.
if __name__ == "__main__":
Un module est presque toujours utile aussi comme programme: on veut pouvoir le lancer directement pour l'essayer. Mais si l'on écrit des appels de démonstration au bout du fichier, ils s'exécuteront aussi à l'importation, ce que personne ne souhaite.
Python offre pour cela une variable spéciale, __name__, qu'il remplit automatiquement dans chaque fichier:
print("__name__ vaut:", __name__)
Lancé directement par python3 qui_suis_je.py, ce fichier affiche:
__name__ vaut: __main__
Importé, il affiche autre chose. Voici le fichier qui l'importe, importateur.py:
import qui_suis_je
print("dans le programme principal, __name__ vaut:", __name__)
__name__ vaut: qui_suis_je
dans le programme principal, __name__ vaut: __main__
La première ligne vient du module importé, la seconde du programme qui l'importe.
La règle est donc simple: __name__ vaut la chaîne "__main__" dans le fichier lancé, et le nom du module dans un fichier importé. Il suffit de tester:
if __name__ == "__main__":
essai = [2.4, 5.1, 7.8]
print("test moyenne:", moyenne(essai))
print("test etendue:", etendue(essai))
test moyenne: 5.1
test etendue: 5.4
Ajoutées à la fin de statistiques.py, ces quatre lignes s'exécutent quand on lance python3 statistiques.py, et pas quand principal.py fait import statistiques — la sortie de principal.py ci-dessus le montre: aucune trace de l'auto-test. C'est la manière normale de loger dans un module ses essais, sa démonstration ou son programme principal, et vous la trouverez dans presque tout le code Python que vous lirez.
Le fichier outils.py se termine par une ligne print qui affiche le mot «chargement», sans protection. Que se passe-t-il quand un autre fichier exécute import outils?
Un programme complet
Assemblons tout: un programme qui lit un fichier CSV de relevés, valide chaque ligne, calcule les statistiques et écrit un rapport. Le fichier d'entrée, releves.csv, contient les sept jours de la semaine — toujours les mêmes données fictives — et deux lignes produites par un second capteur défaillant, l'une illisible, l'autre absurde:
jour;temperature
lundi;2,4
mardi;5,1
mercredi;7,8
jeudi;6,2
vendredi;3,9
samedi;-0,5
dimanche;1,7
capteur 2;n/d
capteur 2;999,9
Le programme s'appelle rapport_meteo.py. Il commence par ses importations, deux constantes qui bornent le domaine physique acceptable, et la fonction de conversion qui refuse ce qui sort de ce domaine:
# fichier rapport_meteo.py
import csv
import sys
MINIMUM = -60.0
MAXIMUM = 60.0
def convertir(texte):
"""Convertit un champ en flottant, ou leve ValueError."""
valeur = float(texte.strip().replace(",", "."))
if valeur < MINIMUM or valeur > MAXIMUM:
raise ValueError(f"valeur hors domaine: {valeur
Deux erreurs possibles, un seul type: float lève déjà une ValueError sur "n/d", et nous en levons une nous-mêmes sur 999.9. C'est volontaire — l'appelant n'a pas à distinguer les deux cas, il rejette la ligne dans les deux cas.
La lecture, ensuite. Chaque ligne est traitée dans son propre try, de sorte qu'une ligne abîmée n'interrompe pas le parcours; le numéro de ligne est retenu pour que le rapport puisse désigner la coupable:
def lire_releves(chemin):
"""Renvoie les couples retenus et les lignes rejetees."""
retenus = []
rejets = []
with open(chemin, "r", newline="") as fichier:
lecteur = csv.reader(fichier, delimiter=";")
next(lecteur) # on saute l'en-tete
numero = 1
for ligne in lecteur:
numero =
La ligne jour, texte = ligne fait double emploi: elle nomme les deux champs, et elle vérifie qu'il y en a exactement deux — une ligne à un ou trois champs lève une ValueError, attrapée comme les autres. Une seule instruction, deux contrôles.
Le calcul, sans surprise, mais avec un refus explicite du cas dégénéré:
def statistiques(retenus):
"""Moyenne, maximum, minimum et nombre de jours de gel."""
temperatures = []
for jour, valeur in retenus:
temperatures.append(valeur)
if len(temperatures) == 0:
raise ValueError("aucun releve exploitable")
gel = 0
for valeur in temperatures:
if valeur < 0:
gel = gel + 1
L'écriture du rapport. Le mode est "w" et le fichier de sortie est différent du fichier d'entrée: le rapport écrase le rapport précédent, jamais les données:
def ecrire_rapport(chemin, retenus, rejets):
"""Ecrit le rapport (mode w: le fichier est ecrase)."""
moyenne, maximum, minimum, gel = statistiques(retenus)
with open(chemin, "w") as sortie:
sortie.write("Rapport hebdomadaire (donnees fictives)\n")
sortie.write("=======================================\n")
sortie.write(f"releves retenus : {len(retenus)}\n")
sortie.write(f"moyenne : {moyenne:.2f} degres
Et enfin le programme principal, protégé par le test du nom, qui traite les deux échecs possibles: le fichier d'entrée absent, et le cas où aucune ligne n'a survécu à la validation.
if __name__ == "__main__":
try:
retenus, rejets = lire_releves("releves.csv")
except FileNotFoundError:
print("fichier releves.csv introuvable")
sys.exit(1)
try:
ecrire_rapport("rapport.txt", retenus, rejets)
except ValueError as erreur:
print("rapport impossible:", erreur)
sys.exit(1)
print
Lancé dans le dossier qui contient releves.csv, il affiche:
7 releves retenus
2 lignes rejetees
rapport ecrit dans rapport.txt
et le fichier rapport.txt qu'il vient d'écrire contient:
Rapport hebdomadaire (donnees fictives)
=======================================
releves retenus : 7
moyenne : 3.80 degres
maximum : 7.8 degres
minimum : -0.5 degres
jours de gel : 1
lignes rejetees : 2
ligne 9: could not convert string to float: 'n/d'
ligne 10: valeur hors domaine: 999.9
Les sept relevés retenus sont exactement les sept températures de la semaine, et l'on retrouve la moyenne de 3,80 °C, le maximum de 7,8 °C, le minimum de −0,5 °C et l'unique jour de gel — les deux lignes du capteur défaillant ont été écartées, mais pas silencieusement: le rapport les nomme par leur numéro de ligne, avec le message de l'exception qui les a fait rejeter. C'est la différence entre un programme qui ignore les erreurs et un programme qui les rend compte.
Enfin, lancé depuis un dossier qui ne contient pas releves.csv, le même programme affiche:
fichier releves.csv introuvable
et s'arrête proprement avec le code de retour 1, au lieu d'exhiber une trace de dix lignes à un utilisateur qui n'y peut rien.
Dans lire_releves, le bloc try est à l'intérieur de la boucle for. Que changerait le fait de le placer autour de la boucle entière?
Synthèse
- Un fichier fait survivre les données au programme. Le cycle est toujours le même: lire un fichier d'entrée, traiter en mémoire, écrire un fichier de sortie distinct — jamais le fichier d'entrée.
- On ouvre avec
with open(chemin, mode) as fichier:et jamais autrement: le bloc garantit la fermeture, y compris quand une erreur l'interrompt. Le mode"r"lit,"a"ajoute, et"w"efface le contenu existant dès l'ouverture, sans prévenir. - La lecture par défaut est la boucle
for ligne in fichier:, qui ne charge qu'une ligne à la fois; chaque ligne contient son\nfinal, questrip()enlève. Un CSV suisse se lit aveccsv.reader(fichier, delimiter=";")et ses nombres avecfloat(texte.replace(",", ".")). - Une exception interrompt l'exécution et remonte la chaîne des appels jusqu'au premier
exceptqui nomme son type; tout ce qui se trouvait entre les deux est abandonné.trysurveille,except Xtraite,elses'exécute si rien n'a échoué,finallys'exécute toujours et en dernier. - Un
exceptnomme toujours son type. Unexcept:nu attrape aussi vos fautes de frappe et les transforme en résultats faux et silencieux — la faute la plus coûteuse du chapitre avec le mode"w". raise ValueError(f"…{valeur}")permet à une fonction de refuser une donnée en disant laquelle et pourquoi; c'est l'appelant, et non elle, qui décide de la suite.- Un module est un fichier
.pyque l'on importe; son code s'exécute à l'importation, d'où leif __name__ == "__main__":qui réserve les essais et le programme principal au lancement direct.
Le fichier journal.txt n'existe pas encore. Que fait open(chemin, 'a') suivi d'un write?
Les dix notes du cours — Alice 4,5; Bruno 5,0; Chloe 3,5; David 6,0; Elena 4,0; Farid 5,5; Gaelle 4,5; Hugo 3,0; Ines 5,0; Jonas 4,5 — vivaient jusqu'ici dans le code. Nous allons les écrire dans un fichier CSV, les relire, en tirer un rapport, et comprendre ce qui arrive au rapport quand on relance le programme. L'échelle suisse va de 1 à 6 et la note suffisante est 4,0.
Écrire le fichier
Le programme ouvre notes.csv en mode "w", écrit d'abord la ligne d'en-tête nom;note, puis une ligne par élève au format Alice;4.5.
Combien de lignes le fichier notes.csv contient-il une fois écrit?
Relire et compter
La moyenne, calculée sur les valeurs lues
Relancer le programme
Exercices
Vous pouvez afficher le corrigé directement sous chaque énoncé après avoir cherché la solution.
Écrivez un programme qui ouvre semaine.csv, compte le nombre de lignes et le nombre total de caractères (sans compter les retours à la ligne), affiche les deux nombres, puis ajoute au fichier une ligne de commentaire de la forme # 8 lignes, 90 caracteres. Affichez pour finir la dernière ligne du fichier, afin de vérifier qu'elle a bien été ajoutée et que rien n'a été perdu.
Solution
Le comptage se fait en une passe, dans la boucle de lecture; l'annotation demande une seconde ouverture, en mode "a", parce que le fichier a été lu jusqu'au bout et qu'un fichier ouvert en "r" ne s'écrit pas.
lignes = 0
caracteres = 0
with open("semaine.csv", "r") as fichier:
for ligne in fichier:
À partir de semaine.csv, écrivez un programme qui produit un fichier gel.txt contenant, en première ligne, le nombre de jours dont la température moyenne est négative, puis une ligne par jour concerné au format samedi: -0.5. Relisez gel.txt et affichez-le pour vérifier.
Solution
import csv
gel = []
with open("semaine.csv", "r", newline="") as entree:
lecteur = csv.reader(entree, delimiter=";")
next
Écrivez une fonction convertir_liste(textes) qui reçoit une liste de chaînes et renvoie deux listes: celle des valeurs converties en flottants, et celle des chaînes qu'elle n'a pas su convertir. Les nombres peuvent être écrits à la suisse, avec une virgule décimale. Essayez-la sur ["2,4", "n/d", "5,1", "", "-0,5"].
Solution
def convertir_liste(textes):
"""Renvoie les valeurs converties et les textes refuses."""
valeurs = []
refuses = []
for texte in textes:
try:
valeurs.append(float(texte.replace(",", ".")))
Reprenez le fichier notes.csv du problème guidé — dix lignes nom;note derrière une ligne d'en-tête. Écrivez un programme qui le lit et produit rapport_notes.txt contenant l'effectif, la moyenne à deux décimales, la note minimale, la note maximale et le nombre de notes suffisantes (supérieures ou égales à 4,0). Affichez le rapport obtenu.
Solution
Si vous ne l'avez pas encore, créez le fichier de départ à partir du dictionnaire du chapitre 7:
notes = {"Alice": 4.5, "Bruno": 5.0, "Chloe": 3.5, "David": 6.0, "Elena": 4.0,
"Farid":
Écrivez un module meteo.py contenant deux fonctions. charger(chemin) lit un fichier d'une valeur par ligne et renvoie la liste des flottants; si le fichier n'existe pas, elle le signale et renvoie une liste vide. resume(valeurs) renvoie le triplet moyenne, maximum, minimum, et lève une ValueError si la liste est vide. Le module doit s'auto-tester quand on le lance directement, et rester silencieux quand on l'importe. Écrivez ensuite bilan.py, qui l'utilise sur temperatures.txt puis sur un fichier absent.
Solution
Le module d'abord. Les deux fonctions traitent l'erreur à des niveaux différents, et c'est tout l'intérêt de l'exercice: charger attrape (elle sait quoi faire d'un fichier absent: renvoyer une liste vide), resume lève (elle ne peut pas inventer une moyenne).
"""Lecture et resume d'un fichier de temperatures."""
def charger(chemin):
"""Renvoie les temperatures du fichier, ou [] s'il manque."""
valeurs = []
try:
with open(chemin,
Références
- Downey, Think Python, 3e édition, O'Reilly — chapitres «Files» et «Exceptions»; librement disponible en ligne, et traduit en français.
- Swinnen, Apprendre à programmer avec Python 3, Eyrolles — chapitres sur les fichiers et sur la gestion des exceptions; librement disponible.
- Matthes, Python Crash Course, 3e édition, No Starch Press — chapitre «Files and Exceptions», qui suit le même ordre que celui-ci.
- La documentation officielle Python, section «Errors and Exceptions» du tutoriel, pour la liste complète des blocs et la hiérarchie des types d'exception.
- La documentation officielle Python, module
csv— en particulier la remarque sur l'argumentnewline=""deopenet sur les dialectes. - La documentation officielle Python, section «Modules» du tutoriel, pour l'importation, le chemin de recherche et le rôle de
__name__.