Licence CC BY-SA

Lire un fichier en Python

Avec les modules nous savons déjà lire les fichiers Python, mais seulement eux et pour un traitement bien particulier.

Ici, nous voulons plutôt apprendre à gérer les fichiers présents sur l’ordinateur, comme des documents textes.

Fichiers et dossiers sur l'ordinateur

Pour rappel, un ordinateur organise ses données en fichiers. Il existe des fichiers de tous types : des images, des fichiers de code, des musiques, etc. Un fichier représente un document bien précis sur l’ordinateur.

Chaque fichier se situe dans un dossier (ou répertoire). On peut voir les dossiers comme des classeurs où seraient rangés les fichiers.
Ces dossiers forment une structure hiérarchique sur l’ordinateur : un dossier peut contenir d’autres dossiers, comme des intercalaires dans un classeur, ou des classeurs sur une étagère.
Un fichier appartient alors à un dossier, qui lui-même appartient à un dossier parent, etc. jusqu’à atteindre la racine du système de fichiers.

Pour retrouver un fichier, il est alors courant d’utiliser son chemin. Il s’agit de la hiérarchie de dossiers à parcourir puis du nom du fichier en question. Ce chemin est unique. C’est ce chemin que nous utiliserons dans nos programmes pour accéder aux fichiers.

Sous Windows, un chemin sera généralement de la forme C:\chemin\vers\mon\fichier.txtC:\ représente la racine du système de fichiers.
Sous Linux on verra plutôt /chemin/vers/mon/fichier.txt (où / est la racine).

On dit que ce chemin est le chemin absolu vers le fichier, car il débute par la racine du système, qui permet donc de le retrouver depuis n’importe où.

Mais il est aussi possible de préciser le chemin d’un fichier à partir d’un autre répertoire, on parle alors de chemin relatif.
Par exemple, depuis le répertoire C:\chemin\vers (ou /chemin/vers), le chemin relatif de notre fichier est mon\fichier.txt (ou mon/fichier.txt). Il s’agit du chemin restant à parcourir pour trouver le fichier.

En programmation, nous exécuterons toujours notre code depuis un répertoire particulier, que l’on appellera répertoire courant (généralement le dossier dans lequel sont stockés les fichiers de code). Nous pourrons ainsi référencer nos fichiers par leur chemin absolu, ou par leur chemin relatif par rapport à ce répertoire.

Il est aussi possible dans un chemin relatif d’accéder à un fichier d’un répertoire parent, à l’aide de la syntaxe ...
Par exemple depuis le répertoire C:\chemin\vers\toto (/chemin/vers/toto), on peut accéder à notre fichier fichier.txt via le chemin relatif ..\mon\fichier.txt (../mon/fichier.txt).

Problématique : sauvegarder l'état de notre jeu

Avec notre jeu, nous sommes pour l’instant obligé de faire toute la partie en une fois. Bon, il est assez simpliste et ne consiste que dans un combat.

Mais imaginons que nous le développions pour avoir un système de tournoi, ou développer un RPG autour, alors il serait pratique de pouvoir mettre le jeu en pause. Pour cela, il va falloir d’une manière ou d’une autre enregistrer l’état actuel du jeu afin de le reprendre plus tard.

Et la manière la plus simple de procéder, c’est d’utiliser un fichier : l’état du jeu sera sauvegardé dans le fichier à la fermeture, et rechargé depuis le même fichier au lancement. Nous allons donc dans un premier temps voir comment nous pouvons gérer nos fichiers, et dans un second nous nous intéresserons au format des données.

Fonction open

Nous allons commencer simplement avec un fichier texte. Commencez par créer un fichier hello.txt dans votre répertoire courant, contenant simplement la phrase Hello World!. Vous pouvez utiliser votre éditeur de code pour créer ce fichier.

Sous Windows, l’extension des fichiers n’est pas affichée par défaut. Assurez-vous donc que votre fichier se nomme bien hello.txt (extension comprise) pour que la suite puisse fonctionner correctement.

Depuis Python, nous utiliserons ensuite la fonction open pour ouvrir le fichier, avec comme argument le chemin vers notre fichier. Ici, comme notre fichier se trouve dans le répertoire courant, il nous suffira de faire open('hello.txt').

Pour un fichier dans le répertoire parent, nous aurions par exemple écrit open('../hello.txt'), ou open('subdirectory/hello.txt') pour un répertoire enfant (open('..\hello.txt') ou open('subdirectory\hello.txt') sous Windows).

>>> open('hello.txt')
<_io.TextIOWrapper name='hello.txt' mode='r' encoding='UTF-8'>

On voit que l’appel nous renvoie un objet un peu étrange mais l’essentiel est là : nous avons ouvert un fichier hello.txt encodé en UTF-8 et en mode r.

Qu’est-ce que ce mode ?
Il faut savoir que plusieurs opérations sont possibles pour les fichiers, de lecture et d’écriture. Les différentes opérations impliquent des besoins différents et le système d’exploitation requiert donc un mode lors de l’ouverture du fichier.
Ici, r signifie que nous ouvrons le fichier en lecture seule (read), nous verrons par la suite quels autres modes d’ouverture existent.

La fonction open prend un deuxième argument optionnel pour spécifier ce mode. Il vaut 'r' par défaut, d’où le comportement que nous observons.

>>> open('hello.txt', 'r')
<_io.TextIOWrapper name='hello.txt' mode='r' encoding='UTF-8'>

Ça c’est pour les cas où ça se passe bien. Il se peut aussi que l’ouverture échoue : si le fichier est introuvable ou que les droits sont insuffisants par exemple (pas la permission d’accéder au fichier appartenant à un autre utilisateur). Dans ce cas, une erreur sera levée par la fonction open et le fichier ne sera pas ouvert.

>>> open('notfound.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'notfound.txt'
>>> open('cantread.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
PermissionError: [Errno 13] Permission denied: 'cantread.txt'

Dans le cas où vous rencontriez ces erreurs pour un fichier qui devrait être bon, assurez-vous donc toujours que vous êtes dans le bon répertoire et que l’utilisateur a les droits suffisants pour lire le fichier.

Fichiers

Lire le contenu d’un fichier

Avoir ouvert un fichier, c’est bien, mais ce qui nous intéresse ici est son contenu. Nous allons pour cela nous intéresser à l’objet renvoyé par open.

Il s’agit d’un objet de type TextIOWrapper, c’est ainsi que Python identifie un fichier textuel. Cet objet possède différentes méthodes, et notamment la méthode read. Utilisée sans argument, elle renvoie le contenu complet du fichier sous forme d’une chaîne de caractères.

>>> f = open('hello.txt')
>>> f.read()
'Hello World!\n'

On remarque ici que mon fichier se termine par un saut de ligne, cela fait partie du contenu du fichier.

Sous Windows, il est possible que votre fichier se termine par \r\n, qui est la représentation d’un passage à la ligne sur ce système.

Mais l’objet que nous avons en Python n’est pas à proprement parler un fichier, c’est une entité qui enrobe les opérations possibles sur le fichier, on parle de wrapper. Et celui-ci ne représente qu’un curseur qui avance dans le fichier présent sur le système. Ainsi, l’état d’un fichier évolue au fur et à mesure qu’on le parcourt.

À l’ouverture, le curseur se trouvait naturellement au début du fichier. Mais une fois le contenu lu, celui-ci s’est déplacé — comme sur une bande d’enregistrement qui défilerait — et se trouve maintenant à la fin. Ne vous étonnez donc pas si vous tentez un nouveau read sur le même fichier et obtenez une chaîne vide.

>>> f.read()
''

L’explication est que la fonction lit le contenu à partir de là où se trouve le curseur dans le fichier, et en l’occurrence il n’y a plus rien à lire.

Une seule lecture suffit généralement à traiter le contenu du fichier, mais il peut arriver dans certains cas que l’on veuille revenir en arrière. Il existe pour cela la méthode seek prenant une position dans le fichier pour y déplacer le curseur. 0 correspond au début du fichier.

>>> f.seek(0)
0
>>> f.read()
'Hello World!\n'

Mais une autre position dans le fichier serait aussi valide.

>>> f.seek(6)
6
>>> f.read()
'World!\n'
Fermer un fichier

Un tel curseur sur un fichier représente une ressource au niveau du système d’exploitation, et les ressources sont limitées. Le nombre de fichiers qu’un programme peut ouvrir va dépendre de la machine et du système, il est par exemple de 1024 chez moi. C’est-à-dire que chaque programme ne peut ouvrir plus de 1024 fichiers simultanément.

Vous me direz que nous en sommes encore loin mais toujours est-il qu’il n’est pas utile de gaspiller ces ressources. Ainsi, nous prendrons l’habitude de libérer notre ressource dès que nous aurons terminé de travailler avec elle.

Cela se fait par exemple avec un appel à la méthode close sur le fichier.

>>> f.close()

La méthode ne renvoie rien, tout s’est bien passé, la ressource est maintenant libérée sur le système.

Si nous essayons à nouveau de réaliser une opération sur notre fichier (read, seek), nous obtiendrons une erreur comme quoi le fichier est fermé. Python n’a en effet plus de référence vers le fichier et il faudrait l’ouvrir à nouveau (avec un appel à open) pour retravailler dessus.

>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
Bloc with

Néanmoins, l’appel explicite à close n’est pas la manière à privilégier pour libérer la ressource. Prenons par exemple la fonction suivante, pour récupérer le contenu d’un fichier sous forme d’un nombre entier (int).

def get_file_number(filename):
    f = open(filename)
    content = f.read()
    value = int(content)
    f.close()
    return value

À l’usage, sur un fichier number.txt contenant le texte 42, elle fonctionne très bien.

>>> get_file_number('number.txt')
42

Mais si on tente de l’exécuter avec notre fichier hello.txt (qui ne contient pas un nombre) on obtient logiquement une erreur.

>>> get_file_number('hello.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in get_file_number
ValueError: invalid literal for int() with base 10: 'Hello World!\n'

L’erreur survient à la ligne 4 de notre fonction, value = int(content). À cet instant, l’exécution de la fonction s’arrête pour remonter l’erreur survenue.
La ligne suivante, f.close() n’a donc pas pu être exécutée, et ne le sera pas. C’est tout de même problématique.

Il y a des mécanismes pour traiter les erreurs et gérer des cas comme celui-ci (voir chapitres suivants), mais le plus simple est encore de ne pas avoir à faire l’appel à close nous-même.

Pour cela il existe en Python ce qu’on appelle des gestionnaires de contexte qui permettent de facilement traiter les ressources. Ils prennent la forme d’un bloc with, suivi par l’expression récupérant la ressource (ici l’appel à open). Le mot-clé as permet ensuite de récupérer cette ressource dans une variable.

with open('hello.txt') as f:
    print(f.read())

Le code précédent est ainsi équivalent à :

f = open('hello.txt')
print(f.read())
f.close()

À l’exception que le close sera réalisé dans tous les cas, même si le read échoue par exemple.

Le code de notre fonction get_file_number deviendrait donc :

def get_file_number(filename):
    with open(filename) as f:
        content = f.read()
        return int(content)

Et on observe le même comportement que précédemment à l’utilisation.

>>> get_file_number('number.txt')
42
>>> get_file_number('hello.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in get_file_number
ValueError: invalid literal for int() with base 10: 'Hello World!\n'

L’erreur survient toujours, mais cette fois-ci la ressource a correctement été libérée, le mécanisme est géré par Python.

Quand vous manipulez des fichiers, utilisez donc toujours un bloc with pour éviter les soucis.