Lecture et écriture de fichiers
Lire et écrire avec `with open`, ce qui sépare r/w/a/x, le saut de ligne à la fin de chaque ligne, pourquoi `encoding` s'écrit toujours, et pathlib pour les petites tâches.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Tout ce que nous avons construit sur vingt-deux chapitres a disparu au moment où le programme s'est terminé.
basket = []
basket.append("pen")
print(basket)['pen']Relancez-le et le panier est encore vide. Listes, dictionnaires, fonctions — tout cela vit en mémoire, et la mémoire ne dure que le temps du programme.
Le chapitre précédent parlait du partage de code entre programmes. Celui-ci parle de données — comment une exécution laisse son travail à la suivante, ou à quelqu'un d'autre.
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("first line\n")
fh.write("second line\n")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(fh.read(), end="")first line
second lineLe programme est terminé, et notes.txt est toujours là.
À la fin de ce chapitre vous pourrez
- Lire et écrire des fichiers avec
with open(...), et dire pourquoiwith - Dire ce qui sépare
"r","w","a"et"x"— surtout ce que"w"détruit - Lire un fichier une ligne à la fois, et gérer le
\nà la fin de chaque ligne - Dire pourquoi
encoding="utf-8"s'écrit toujours - Utiliser
pathlib.Pathpour les petites tâches - Lire un
FileNotFoundErroret unUnicodeDecodeError
Prérequis : Modules et imports.
Pourquoi with
Ouvrir un fichier emprunte quelque chose au système d'exploitation, et un emprunt doit être remboursé. Sans fermeture, ce que vous avez écrit n'atteindra peut-être jamais le disque, et assez de fichiers non fermés arrêteront le programme.
Un bloc with effectue ce remboursement tout seul — le fichier est fermé quand le bloc se termine, même si quelque chose à l'intérieur a levé une exception.
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(fh.closed)
print(fh.read())True
ValueError: I/O operation on closed file.(Avant la ligne d'erreur, Python imprime aussi quelques lignes de traceback ; dans ce chapitre, seule la dernière ligne est montrée.)
Le nom fh existe toujours après le bloc — la portée fonctionne exactement comme avant — mais le fichier est fermé. Dans ce cours, open() s'écrit toujours avec with.
Lecture
.read() donne tout le contenu d'un seul coup :
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(repr(text))
print(len(text))'first line\nsecond line\n'
23repr est employé exprès, parce qu'un simple print cache les sauts de ligne. Un fichier est un seul long morceau de texte, avec ses fins de ligne à l'intérieur sous forme de \n — s'en souvenir, c'est la moitié de ce chapitre.
Une ligne à la fois
.read() est un mauvais choix pour un gros fichier, parce qu'il charge tout en mémoire. En bouclant sur le fichier lui-même, Python lit une ligne à la fois :
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'Chaque ligne se termine par \n. C'est la confusion la plus courante qui soit — line == "pen" ne correspond pas, "pen\n" et "pen" sont deux clés de dictionnaire différentes, et la raison est invisible. (int() et float() ignorent eux-mêmes les espaces et le \n autour du nombre, donc int("3\n") donne bien 3 — mais une chaîne est conservée exactement telle quelle, donc le \n reste dans les comparaisons et dans la sortie imprimée.)
Alors le premier geste est presque toujours .strip() :
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
name, price, qty = line.strip().split(",")
print(f"{name:<5} {float(price) * int(qty):>8.2f}")pen 45.00
bag 850.00
ink 240.00.strip() puis .split(",") puis float() et int() — cette séquence reviendra encore et encore quand vous lirez des fichiers. Tout ce qui sort d'un fichier est du texte, jamais des nombres ; la conversion du chapitre sept est nécessaire ici aussi.
Quand on veut toutes les lignes d'un coup, il y a deux méthodes :
with open("orders.txt", "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
print(lines)['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2'].splitlines() supprime les fins de ligne. Il y a aussi .readlines(), mais elle garde le \n — c'est là que la plupart des gens se font piéger.
Écriture, et une lettre dangereuse
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("new\n")Le fichier contenait auparavant old content. Maintenant :
new"w" vide le fichier — au moment de l'ouverture, avant que quoi que ce soit soit écrit. Aucun avertissement, et aucun retour en arrière.
Pour garder ce qui est là et ajouter à la fin, "a" :
with open("notes.txt", "a", encoding="utf-8") as fh:
fh.write("new\n")old content
newEt pour refuser d'écrire si le fichier existe déjà, "x" :
FileExistsError: [Errno 17] File exists: 'notes.txt'Les quatre modes en une ligne : "r" lit, "w" efface et écrit, "a" ajoute à la fin, "x" ne fait que créer.
Faites une pause avant de taper"w". Des gens perdent ainsi des heures de travail à cause de cette seule lettre. Si le fichier que vous ouvrez n'a pas été créé par votre programme, réfléchissez avant d'écrire"w".
C'est à vous de fournir le \n
print passe à la ligne pour vous. write non.
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)'penbag'La façon la plus propre d'écrire une liste de lignes est donc :
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
fh.write("\n".join(rows) + "\n")'pen\nbag\n'"\n".join(rows) met les retours entre les lignes, et le + "\n" final termine le fichier par un saut de ligne — c'est la règle habituelle pour les fichiers texte, et ce que beaucoup d'outils attendent.
encoding="utf-8" — toujours
Sans encoding, Python utilise la valeur par défaut du système d'exploitation, qui change d'une machine à l'autre. Le résultat : un programme qui marche là où vous êtes et casse là où se trouve quelqu'un d'autre.
Lire avec le mauvais encodage donne :
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byteÉcrivez donc encoding="utf-8" chaque fois que vous écrivez open() — pour la lecture comme pour l'écriture. La seule exception est le mode binaire ("rb", "wb"), où la question ne se pose pas puisqu'il n'y a pas d'encodage.
pathlib — un chemin court pour les petites tâches
Pour un petit fichier, tout le bloc with peut sembler bien lourd.
from pathlib import Path
path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))
out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")True
3
doneread_text et write_text ouvrent et ferment le fichier en interne, donc aucun with n'est nécessaire. Path fait plus que ça — .exists(), .name, .suffix, .parent — et les chemins de dossiers se joignent avec /, comme dans Path("data") / "orders.txt".
Lequel, et quand ? Path quand on veut tout le fichier d'un coup, with open(...) quand on lit ligne par ligne. Gardez une chose à l'esprit : write_text se comporte toujours comme "w" — si le fichier existe déjà, son ancien contenu est effacé sans aucun avertissement. write_text n'a aucun moyen d'ajouter à la fin ; pour cela il faut with open(..., "a").
Un exemple complet
orders.txt :
pen,15.0,3
bag,850.0,1
ink,120.0,2main.py :
"""Read an order file, total it, and write a report beside it."""
from pathlib import Path
TAX_RATE = 0.15
def read_order(path):
"""Each line is name,price,quantity. Blank lines are skipped."""
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
name, price, quantity = raw.split(",")
lines.append((name, float(price), int(quantity)))
return lines
def report(lines):
rows = []
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
rows.append("-" * 20)
rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
return rows
def main():
order = read_order(Path("orders.txt"))
rows = report(order)
Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"), end="")
print()
print("lines read:", len(order))
if __name__ == "__main__":
main()pen 3 51.75
bag 1 977.50
ink 2 276.00
--------------------
total 1305.25
lines read: 3Quatre choses qui valent le coup d'être regardées.
Trois fonctions avec trois tâches séparées. read_order lit depuis le disque, report ne fait que calculer, et main écrit. report ne reçoit aucun fichier du tout — on lui donne une liste et elle renvoie une liste. La tester n'exige donc aucun fichier, et au chapitre vingt-huit ce sera exactement ce qui aidera.
Les lignes vides sont traitées à l'avance. Un seul \n à la fin du fichier ne produit pas de ligne vide — .splitlines() le supprime. Mais dans un fichier édité à la main, une vraie ligne vide (deux \n à la suite) au milieu ou à la fin est tout à fait normale, et sans la gérer raw.split(",") n'obtiendrait pas trois éléments et lèverait un ValueError. if not raw.strip(): continue règle l'affaire en une ligne.
float(price) et int(quantity) ne sont pas optionnels, parce que tout ce qui sort d'un fichier arrive sous forme de texte. Oubliez-les et price * quantity répéterait silencieusement la chaîne à la place — le pire des bugs, celui qui ne casse pas.
Le rapport va dans un fichier séparé. orders.txt n'est jamais ouvert avec "w" — le fichier d'entrée reste intact. C'est une habitude qui vaut la peine de se former : n'écrivez pas dans ce que vous lisez.
Quand ça casse
FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' Le fichier n'est pas là, ou vous lancez le programme depuis un autre dossier. Le chemin est calculé à partir de l'endroit où vous l'avez lancé, pas du dossier du script. Imprimez Path("orders.txt").resolve() pour voir où il cherche.
UnicodeDecodeError: 'utf-8' codec can't decode byte ... Le fichier a été écrit avec un autre encodage, ou ce n'est pas un fichier texte du tout. Corrigez le encoding, ou ouvrez-le avec "rb" s'il est binaire.
ValueError: I/O operation on closed file. Le fichier a été utilisé à l'extérieur de son bloc with. Lisez ce dont vous avez besoin à l'intérieur du bloc.
ValueError: not enough values to unpack Une ligne n'a pas les virgules que vous attendiez — souvent une ligne vide. Sautez les lignes vides, et dans le doute print(repr(raw)).
J'ai écrit le fichier mais il est vide On le lit avant que le bloc with ne soit terminé. Les écritures atteignent le disque quand le fichier est fermé.
Toutes mes lignes se sont collées en une seule write ne passe pas à la ligne. C'est à vous de fournir le "\n".
Tout mon fichier a disparu Il a été ouvert avec "w". Utilisez "r" pour lire et "a" pour ajouter à la fin.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
repr est utilisé, donc rien n'est caché. Qu'est-ce qui s'imprime ?
# orders.txt
# pen,15.0,3
# bag,850.0,1
# ink,120.0,2
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))- A'pen,15.0,3' 'bag,850.0,1' 'ink,120.0,2'
- B'pen,15.0,3\nbag,850.0,1\nink,120.0,2\n'
- C'pen,15.0,3\n' 'bag,850.0,1\n' 'ink,120.0,2\n'
- D['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']
Le fichier est ouvert avec "w" mais rien n'est écrit. Qu'est-ce qui s'imprime ?
# notes.txt already contains: important work
with open("notes.txt", "w", encoding="utf-8") as fh:
print("opened")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- Aopened 'opened\n'
- BUn `FileExistsError`
- Copened 'important work\n'
- Dopened ''
Deux choses sont écrites séparément. Qu'y a-t-il dans le fichier ?
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)
with open("out.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- A'pen\nbag\n'
- B'pen\nbag'
- C'pen bag'
- D'penbag'
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
Créez un fichier appelé people.txt avec un nom et un nombre sur chaque ligne, séparés par une virgule — au moins quatre lignes, et laissez exprès une ligne vide au milieu.
Écrivez ensuite summary.py avec trois fonctions : une qui lit le fichier dans une liste de paires, une qui transforme cette liste en lignes de texte (qui a le plus grand nombre, et le total), et main qui écrit le tout dans summary.txt. N'oubliez pas if __name__ == "__main__":.
Puis cinq expériences :
- Enlevez le
.strip(), puis imprimez la fin de chaque ligne avecrepr().int()lève-t-il une erreur ? (Non —int("10\n")donne10.) Alors où se cache le\n, et que se passe-t-il sur la ligne vide ? - Supprimez un nombre d'une ligne pour que la virgule soit là mais pas le nombre. Lisez le message.
- Changez
"w"en"a"dansmain, puis lancez le programme trois fois. Qu'est-il arrivé àsummary.txt? - Essayez d'ouvrir
people.txtavec"w"... ne le faites pas. Faites-en une copie et faites-le dessus plutôt, puis regardez ce qu'il reste. - Imprimez
Path("people.txt").resolve(), puis lancez le programme depuis un autre dossier. Le chemin change-t-il ?
Ce dernier point explique une erreur que presque tout le monde fait une fois : le programme marchait, puis il a été lancé d'un autre endroit et a levé un FileNotFoundError.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz