En Python, vous avez sûrement été amenés à manipuler des fichiers pour lire ou sauvegarder des données. Dans ce cadre, le context manager (with) et la fonction open sont incontournables pour éviter les fuites de mémoire.
De plus, les différents modes r, w, a, r+, w+ et a+ sèment souvent la confusion.
Pourquoi utiliser with open() ? Le context manager
En tant que débutant, on pourrait avoir tendance à ouvrir un fichier, faire nos opérations et, si on y pense, le fermer. En effet, il est tout à fait possible d'ouvrir et d'écrire dans un fichier sans passer par un context manager, bien que l'on utilise toujours la fonction open.
fichier = open("donnees.txt", "r", encoding="utf-8") contenu = fichier.read() fichier.close()
Si on oublie de fermer le fichier avec la méthode close(), ou si une erreur survient avant cette ligne, il y a un risque de fuite de mémoire.
La solution à ce problème : le context manager 🔥, méthode qui est d'ailleurs recommandée par la PEP 8.
with open("donnees.txt", "r", encoding="utf-8") as fichier: contenu = fichier.read() # Traitement des données...
Le context manager s'occupe de l'allocation et de la libération des ressources pour nous : c'est propre et c'est sûr.
À noter
C'est une bonne pratique de spécifier l'encodage : encoding="utf-8". Cela permet d'éviter les problèmes d'accents, surtout lors d'échanges de fichiers entre Windows et Linux/macOS.
Les modes de base : lecture (r), écriture (w) et ajout (a)
Dans l'exemple précédent, nous avons vu que la fonction open prend un second argument : le mode. C'est là que ça devient intéressant 😅.
À noter
Avant de parler plus en détail des modes, nous allons aborder la notion de curseur. C'est l'endroit où Python va commencer à lire ou à écrire dans votre fichier. Selon le mode choisi, le curseur se positionnera différemment.
Le mode lecture (r)
Il s'agit du mode par défaut. Il permet seulement de lire le fichier et, si celui-ci n'existe pas, Python lève une erreur FileNotFoundError.
Le curseur est placé au début du fichier.
with open("mon_fichier.txt", "r", encoding="utf-8") as f: contenu = f.read()
Le mode écriture (w)
Il permet d'écrire dans le fichier. Si le fichier existe, il est écrasé et, s'il n'existe pas, il est créé.
Le curseur est placé au début.
with open("mon_fichier.txt", "w", encoding="utf-8") as f: f.write("Ceci est un nouveau contenu.")
Le mode ajout (a)
Contrairement au mode précédent, le mode ajout permet d'ajouter du contenu sans effacer l'existant. Si le fichier n'existe pas, il est créé.
Le curseur est placé automatiquement à la fin du fichier.
with open("logs.txt", "a", encoding="utf-8") as f: f.write("\nNouvelle entrée dans le journal.")
Jusqu'ici, tout allait bien. Maintenant, les choses vont se compliquer un peu... Nous allons nous pencher sur les modes avancés.
Le mode lecture et écriture (r+)
Ce mode permet de lire et d'écrire sans effacer le fichier. Comme pour le mode r, le fichier doit exister et le curseur est placé au début.
Ainsi, si vous écrivez dans le fichier, vous allez commencer l'écriture depuis le début, ce qui écrasera le texte déjà présent caractère par caractère.
# Imaginons un fichier contenant "Python" with open("fichier.txt", "r+", encoding="utf-8") as f: f.write("Cy") # Le fichier contient maintenant "Cython" # "Py" a été remplacé par "Cy"
Le mode écriture et lecture (w+)
Dès l'ouverture, le contenu du fichier est effacé. Cela permet de repartir d'une feuille blanche, d'écrire des données et de les relire dans la foulée. Après l'écriture, il faut penser à replacer le curseur au début du fichier.
# Imaginon un fichier texte avec Python écrit dedans. with open("fichier.txt", "w+", encoding="utf-8") as f: print(f.read()) # Affiche une chaîne vide, car w+ efface le contenu du fichier f.write("Nouveau texte") f.seek(0) # Indispensable pour relire ce qu'on vient d'écrire print(f.read()) # Affiche "Nouveau texte"
Le mode ajout et lecture (a+)
Dans ce cas, le curseur est placé à la fin. Par conséquent, si vous cherchez à lire directement le fichier, vous n'obtiendrez rien, puisque vous êtes déjà au bout du document. L'écriture se fera toujours à la fin : même si vous déplacez le curseur avec seek() auparavant, l'écriture se fera quand même à la fin.
# Imaginons un fichier texte avec Python with open("fichier.txt", "a+", encoding="utf-8") as f: print(f.read()) # Affiche une chaîne vide car le curseur est à la fin du fichier f.write("Ajout") f.seek(0) # On revient au début pour lire tout le fichier print(f.read()) # Affiche "Python Ajout" car c'est le contenu du fichier après l'ajout
Qu'est-ce que seek() ?
La méthode seek() permet de déplacer le curseur pour la lecture ou l'écriture.
Revenons sur la syntaxe file.seek(offset, whence) :
-
offsetcorrespond au nombre d'octets de déplacement (en mode texte, vous utiliserez principalement0pour revenir au début du fichier) -
whencepermet de définir le point de référence
Le point de référence (whence) fonctionne ainsi :
-
0pour le début du fichier (valeur par défaut) -
1pour la position actuelle -
2pour la fin du fichier
Attention
Jusqu'ici, nous avons ouvert nos fichiers en mode texte (ce qui est le mode par défaut). Très pratique, ce mode impose toutefois quelques restrictions avec la méthode seek(). En mode texte, il n'est pas possible de faire un déplacement relatif depuis la position courante ou depuis la fin du fichier.
En fait, en cas de déplacement arbitraire dans un fichier texte, le curseur peut tomber au milieu d'un caractère qui prend plusieurs octets.
with open("fichier.txt", "r", encoding="utf-8") as f: # Python ne peut pas garantir qu'on ne tombe pas au milieu d'un caractère f.seek(-6, 2) # io.UnsupportedOperation: can't do nonzero end-relative seeks
with open("fichier.txt", "r", encoding="utf-8") as f: f.seek(1, 1) # io.UnsupportedOperation: can't do nonzero cur-relative seeks
Le mode binaire
Pour une liberté totale de déplacement, ou pour travailler avec des fichiers non textuels, vous devez utiliser le mode binaire.
Il suffit simplement d'ajouter la lettre b au mode d'ouverture (exemple : rb).
Si vous avez bien suivi jusqu'ici, la méthode read() vous renverra un objet bytes et non plus une chaîne de caractères.
# Exemple : Lire les 6 derniers octets d'un fichier with open("fichier.txt", "rb") as f: data = f.read() print(type(data)) # Affiche <class 'bytes'> print(data) # Affiche des bytes : b'Python est g\xc3\xa9nial' # On peut se déplacer librement depuis la fin (2) # Reculer de 6 octets f.seek(-6, 2) data = f.read() # Si on sait que c'est du texte, on peut décoder manuellement print(data.decode("utf-8")) # Affiche "énial"