Les fichiers

12-01-2022

Objectifs

  • savoir parcourir en lecture tout ou partir d’un fichier texte

  • savoir produire des fichiers textes

  • connaître les canaux standards stdin et stdout

  • prendre conscience des dangers des données lues

  • découvrir le traitement d’exception.

Pourquoi des fichiers ?

Chaque jour, nous consultons et créons de très nombreux fichiers :

  • consultation de bases de données,

  • photographies,

Les fichiers sont utilisés pour assurer la pesistance et le partage des données.

Lorsqu’avec votre appareil photo numérique vous prenez le portrait de votre grand-mère, le signal lumineux capté est transformé en signal numérique, puis sauvegardé dans un fichier, fichier que vous pouvez ensuite partager avec votre entourage en l’envoyant en pièce jointe d’un courrier électronique ou d’un SMS.

Différents types de fichiers

On distingue deux types de fichiers :

  • les fichiers textes

  • et les fichiers binaires.

Nous commençons par présenter ces deux types.

Fichiers textes

Les fichiers textes sont ceux contenant … du texte.

Ils sont utilisés dans de très nombreux contexte dont voici quelques uns :

  • fichiers textes bruts (plain text)

  • fichiers source de programmes

  • fichiers HTML, CSS

  • fichiers CSV (format tableur textuel)

La structure que partagent tous ces formats de fichiers textes est celle de ligne. Une ligne est une chaîne de caractères terminée par un marqueur de fin de ligne. Ce marqueur de fin de ligne peut différer d’un système d’exploitation à l’autre. Dans les systèmes de type Unix (GNU-Linux et Mac OSX par exemple), ce marqueur est par défaut constitué d’un caractère ASCII de code 10 (0x0A) appelé LINE FEED. Dans les divers systèmes MS-Windows, ce marqueur de fin de ligne est constitué de deux caractères ASCII de code 13 (0x0D) appelé CARRIAGE RETURN et 10 (0x0A) dans cet ordre.

Le principal outil informatique pour lire/produire un fichier texte est un éditeur de texte.

Fichiers binaires

On déclare binaire tout fichier qui n’est pas un fichier texte.

  • fichiers exécutables résultant de la compilation d’un fichier texte source

  • fichiers archives (formats ZIP, TGZ, …)

  • fichiers images (formats PNG, JPG, GIF, TIFF, …)

  • fichiers sons (formats WAV, OGG, FLAC, MP3, …)

  • fichiers videos (formats AVI, MPEG, …)

Contrairement aux fichiers textes pour lesquels la structure de ligne est une structure commune, il n’y a pas de structure commune à tous les fichiers binaires. Les outils informatiques pour lire/produire les fichiers binaires dépendent évidemment du format de ces fichiers.

Lire, écrire dans des fichiers en Python

Note

Dans cette partie nous nous limitons essentiellement aux fichiers textes.

Ouverture d’un canal

Avant de pouvoir lire les informations contenues dans un fichier, ou de pouvoir écrire des informations dans un fichier, il est nécessaire d’ouvrir un canal de communication entre le programme et le fichier.

Figure made with TikZ

L’ouverture d’un canal peut se faire

  • en mode lecture, et dans ce cas on ne peut que lire des informations contenues dans le fichier ;

  • en mode écriture, et dans ce cas il n’est possible que d’écrire dans le fichier ;

  • ou enfin en mode lecture et écriture, mode permettant à la fois la lecture et l’écriture d’informations dans le fichier.

En Python, c’est la fonction open qui permet l’ouverture de canaux.

Ouverture en mode lecture

La fonction open prend au moins deux chaînes de caractères en paramètre :

  • d’abord le nom du fichier à ouvrir ;

  • ensuite le mode d’ouverture.

Pour le mode d’ouverture en lecture, ce second paramètre est 'rt' (r pour read et t pour text) ou plus simplement 'r' (par défaut l’ouverture se fait sur des fichiers textes).

>>> entree = open('foo.txt','r')

Avertissement

Si le fichier qu’on veut ouvrir n’existe pas une exception FileNotFoundError est déclenchée.

>>> entree = open('nexiste.pas', 'r')
...
FileNotFoundError: [Errno 2] No such file or directory: 'nexiste.pas'

Ouverture en mode écriture

C’est la chaîne de caractères 'wt' ou plus simplement 'w' donnée en second paramètre de la fonction open qui permet d’ouvrir un canal de communication en écriture vers un fichier texte.

>>> sortie = open('nouveau.txt', 'w')

Avertissement

L’ouverture en écriture d’un canal vers un fichier existant entraîne la perte des données que contenait ce fichier. L’ouverture en mode 'w' est donc une opération potentiellement dangereuse.

C’est pourquoi, Python propose un mode particulier d’ouverture de canal en écriture, qui vérifie préalablement l’existence ou non du fichier qu’on veut ouvrir. C’est le mode x.

En supposant que le fichier existe.bien existe bien, voici ce qu’on obtient par une tentative d’ouverture en mode x :

>>> sortie = open('existe.bien', 'x')
...
FileExistsError: [Errno 17] File exists: 'existe.bien'

Autres modes d’ouverture

En Python, il est possible d’ouvrir des canaux avec d’autres modes que les simples modes de lecture ou d’écriture :

  • le mode ajout (append) qui permet d’ouvrir un canal en écriture vers un fichier existant afin d’ajouter de nouvelles données. Dans le cas d’un fichier texte toute nouvelle opération d’écriture se fait à la suite du texte existant avant l’ouverture. Ce mode d’ouverture est obtenu en mettant le caractère 'a' en deuxième paramètre de la fonction open. Si on utilise ce mode sur un fichier non existant, un nouveau fichier est créé.

  • le mode lecture et écriture qui permet d’ouvrir un canal à la fois en lecture et en écriture vers un fichier existant. Dans la fonction open s’obtient en ajoutant le caractère '+' à la suite de l’un des caractères 'r', 'w', 'x' ou 'a'.

Type d’un canal ouvert sur un fichier texte

Quelque soit le mode d’ouverture d’un canal vers un fichier texte, son type est _io.TextIOWrapper.

>>> type(entree)
<class '_io.TextIOWrapper'>

Fermeture

Tout canal ouvert doit être fermé lorqu’on n’en a plus besoin.

Pour fermer un canal, on utilise la méthode close.

>>> entree.close()
>>> sortie.close()

Avertissement

L’oubli de fermeture d’un canal ouvert en écriture peut entraîner la perte de données.

Forme syntaxique with

Python offre une structure syntaxique permettant d’omettre la commande explicite de fermeture d’un canal (et ainsi l’oubli de cette commande).

Cette forme débute par le mot clé with et utilise le mot clé as :

# instr avant
with open(..., ...) as f:
   # traitement sur f
# instr après

L’identificateur placé après le mot-clé as est le nom donné au canal ouvert par la fonction open. La portée de cette variable est le bloc (indenté) qui suit le with. À la fin de ce bloc, le canal est automatiquement fermé, sans qu’il ne soit nécessaire d’invoquer la méthode close.

De cette façon, les lignes précédentes sont équivalentes aux suivantes :

# instr avant
f = open(..., ...)
# traitement sur f
f.close()
# instr après

Lecture de données

Dans cette partie on s’intéresse aux méthodes de lecture à travers un canal ouvert.

Dans les exemples illustrant ces méthodes, le fichier timoleon.txt est supposé contenir les trois lignes suivantes (et ces trois lignes uniquement) :

Timoleon est un homme politique grec
ayant vécu au IVème siècle av. JC.
Il est connu pour avoir recolonisé la Sicile.

Lecture d’une ligne

La méthode readline que possèdent les canaux ouverts en lecture renvoie la ligne sur laquelle pointe actuellement le canal.

>>> entree = open('timoleon.txt', 'r')
>>> entree.readline()
'Timoleon est un homme politique grec\n'

Sur cet exemple, la méthode readline renvoie la première ligne du fichier timoleon.txt. Cette ligne est une chaîne de caractères qui contient le marqueur de fin de ligne.

Un deuxième appel à la même méthode va nous donner la seconde ligne :

>>> entree.readline()
'ayant vécu au IVème siècle av. JC.\n'

et un troisième appel donne la troisième ligne :

>>> entree.readline()
'Il est connu pour avoir recolonisé la Sicile.\n'

Ces trois appels identiques produisant des résultats différents montrent qu’un canal possède un état qui change après chaque lecture. Cet état variable indique en permanence à quel endroit dans le fichier la lecture est arrivée.

Après ces trois lignes lues, l’état dans lequel se trouve le canal de lecture est la fin du fichier. Que se passe-t-il si nous invoquons encore la méthode readline ?

>>> entree.readline ()
''

Nous constatons qu’une fois arrivé en fin de fichier, readline renvoie une chaîne de caractères vide.

Avertissement

Il ne faut pas confondre la chaîne vide renvoyée par readline et une ligne vide contenue dans le fichier texte.

En effet, si l’état du canal pointe vers une ligne vide du fichier, la méthode readline renvoie une chaîne de caractères contenant le seul marqueur de fin de ligne : '\n', chaîne qui n’est donc pas vide.

Lorsque readline renvoie une chaîne vide, c’est qu’on est arrivé à la fin du fichier. C’est une caractéristique qu’il est possible d’exploiter en programmation.

Fermons le canal avant de présenter la prochaine méthode.

>>> entree.close()

Lecture de toutes les lignes

La méthode readlines donne la liste de toutes les lignes restant à lire. Si le canal vient d’être ouvert, cela revient à lire toutes les lignes du fichier.

>>> entree = open('timoleon.txt', 'r')
>>> entree.readlines()
['Timoleon est un homme politique grec\n',
 'ayant vécu au IVème siècle av. JC.\n',
 'Il est connu pour avoir recolonisé la Sicile.\n']

On obtient la liste de toutes les lignes contenues dans le fichier timoleon.txt. L’état du canal pointe alors vers la fin du fichier.

Fermons le canal avant de présenter la dernière méthode de lecture.

>>> entree.close()

La méthode read

Terminons par une troisième méthode de lecture qui ne respecte pas la structure en lignes des fichiers textes. Cette méthode, nommée read, peut s’employer avec ou sans paramètre.

Avec paramètre, il faut lui donner le nombre de caractères qu’on désire lire dans le fichier.

>>> entree = open('timoleon.txt', 'r')
>>> entree.read(3)
'Tim'
>>> entree.read(20)
'oleon est un homme p'
>>> entree.read(30)
'olitique grec\nayant vécu au IV'
>>> entree.read(0)
''

Sans paramètre, la chaîne de caractères renvoyée par read est constituée de la totalité des caractères contenus dans le fichier depuis l’état courant du canal.

>>> entree.read()
'ème siècle av. JC.\nIl est connu pour avoir recolonisé la Sicile.\n'

Impossible de lire

Il est impossible de lire (quelle que soit la méthode) dans un fichier ouvert en écriture.

>>> sortie = open('nouveau.txt', 'w')
>>> sortie.readline()
...
UnsupportedOperation: not readable

Écriture de données

Dans cette partie on s’intéresse aux méthodes d’écriture dans un fichier texte.

Écrire des chaînes de caractères

Avec la méthode write, on peut écrire n’importe quelle chaîne de caractères dans un fichier.

>>> sortie = open('nouveau.txt', 'w')
>>> sortie.write('Timoleon est un homme politique')
21
>>> sortie.write(' grec\n' )
6
>>> sortie.close()

Comme cet exemple le montre, la méthode write renvoie le nombre de caractères écrits.

Écrire des lignes

La méthode writelines est une méthode analogue à readlines : elle permet d’écrire dans un fichier une liste de chaînes de caractères. Si chacune de ces chaînes se termine par un marqueur de fin de ligne, le nombre de lignes écrites dans le fichier est égal à la longueur de la liste.

>>> sortie = open('nouveau.txt', 'w')
>>> sortie.writelines(['Timoleon est un homme politique grec\n',
                       'ayant vécu au IVème siècle av. JC.\n',
                                   'Il est connu pour avoir recolonisé la Sicile.\n'])
>>> sortie.close()

Impossible d’écrire

Il est impossible d’écrire (quelque soit la méthode) dans un fichier ouvert en lecture.

>>> entree = open('timoleon.txt', 'r')
>>> entree.write('')
...
UnsupportedOperation: not writable

Exemples

  1. Parcours complet d’un fichier texte (ici cigale.txt) et impression de chacune des lignes sur la sortie standard.

    En voici une première version, avec lecture intégrale du fichier (méthode readlines puis parcours de la liste de lignes obtenue :

    with open('cigale.txt','r') as entree:
       lignes_lues = entree.readlines()
    for ligne in lignes_lues:
       print(ligne, end = '')
    

    Cette version est envisageable pour des textes de taille n’excédant pas les capacités de mémoire de la machine sur laquelle elle est exécutée.

    Note

    On utilise ici le paramètre nommé end de la fonction print auquel on donne la chaîne vide comme valeur, pour empêcher la fonction print de faire un passage à la ligne. En effet, il faut se souvenir que toute ligne lue dans un fichier texte (par l’une ou l’autre des méthodes readline ou readlines) contient le marqueur de fin de ligne (\n).

    Voici une seconde version, qui procède par traitement immédiat d’une ligne qui vient d’être lue :

    with open('cigale.txt', 'r') as entree:
       ligne = entree.readline()
           while ligne != '':
               print(ligne, end='')
               ligne = entree.readline()
    

    Voici une troisième version, variante de la précédente, qui s’appuie sur le fait que les canaux vers des fichiers ouverts en lecture sont itérables :

    with open('cigale.txt', 'r') as entree:
       for ligne in entree:
               print(ligne, end='')
    
  2. Recopie d’un fichier texte dans un autre.

    Première version en faisant une lecture intégrale du fichier avec la méthode read.

    with open('cigale.txt', 'r') as entree:
        tout_lu = entree.read()
    with open ('cigale2.txt', 'w') as sortie:
        sortie.write(tout_lu)
    

    Deuxième version en faisant une lecture intégrale du fichier à copier avec la méthode readlines, et une recopie avec la méthode writelines.

    with open('cigale.txt', 'r') as entree:
        les_lignes = entree.readlines()
    with open ('cigale2.txt', 'w') as sortie:
        sortie.writelines(les_lignes)
    

    Troisième version, en recopiant chaque ligne immédiatement après leur lecture.

    with open('cigale.txt', 'r') as entree:
        with open('cigale2.txt', 'w') as sortie:
                ligne = entree.readline()
                sortie.write(ligne)
    

    Note

    Dans cette troisième version on aurait pu remplacer les deux dernières lignes par la seule ligne :

    sortie.write(entree.readline())
    

Codage des fichiers textes

_images/martine-ecrit-en-utf8.jpg

Les chaînes de caractères de Python3 sont des chaînes Unicode. Lorsqu’elles sont écrites dans un fichier, elles sont encodées. Et lorsqu’elles sont lues depuis un fichier, elles sont décodées.

Il existe plusieurs codages des caractères. Citons-en trois :

  • l”ISO-8859-15, parfois appelé aussi LATIN9, encodage couramment employé en Europe occidentale dans les années 1980-2000, mais qui ne permet pas de coder tous les caractères Unicode ;

  • le cp1252 ou Windows-1252, encodage utilisé par défaut sur le système d’exploitation Windows, qui étant une variante mineure de l’ISO-8859 ne permet pas non plus de coder tous les caractères Unicode ;

  • l”UTF-8, encodage développé dans les années 1990, très utilisé actuellement, qui permet de coder tous les caractères Unicode.

Si on ne le précise pas, le codage employé par la fonction open est celui par défaut de la plateforme sur laquelle le programme s’exécute :

  • aujourd’hui, avec les systèmes GNU-Linux : UTF-8 ;

  • avec les systèmes Windows : cp1252.

La fonction open possède un paramètre optionnel nommé encoding qui permet de préciser le codage voulu.

Exemple

Considérons une même chaîne de caractères (dont l’auteur est Gilles Esposito-Farèse) écrite dans deux fichiers texte, mais avec des codages différents :

>>> texte = "Dès Noël où un zéphyr haï me vêt de glaçons würmiens je dîne d'exquis rôtis de bœuf au kir à l'aÿ d'âge mûr & cætera !"
>>> with open('texte_utf8.txt', 'w', encoding='utf_8') as sortie:
        sortie.write(texte + '\n')
>>> with open('texte_iso8859_15.txt', 'w', encoding='iso8859_15') as sortie:
        sortie.write(texte + '\n')

Maintenant lisons cette chaîne dans chacun des deux fichiers en employant le bon décodeur.

>>> with open('texte_utf8.txt', 'r', encoding='utf_8') as entree:
        lu1 = entree.readline()
>>> print(lu1)
Dès Noël où un zéphyr haï me vêt de glaçons würmiens je dîne d'exquis rôtis de bœuf au kir à l'aÿ d'âge mûr & cætera !

>>> with open('texte_iso8859_15.txt', 'r', encoding='iso8859_15') as entree:
        lu2 = entree.readline()
>>> print(lu2)
Dès Noël où un zéphyr haï me vêt de glaçons würmiens je dîne d'exquis rôtis de bœuf au kir à l'aÿ d'âge mûr & cætera !

>> lu1 == lu2
True

Tout va bien ! Mais si nous employons le mauvais décodeur, on peut obtenir des caractères bizarres :

>>> with open('texte_utf8.txt', 'r', encoding='iso8859_15') as entree:
        lu3 = entree.readline ()
>>> print(lu3)
DÚs Noël où un zéphyr haï me vêt de glaçons wÃŒrmiens je dîne d'exquis rÃŽtis de bœuf au kir à l'aÿ d'âge mûr & cÊtera !

ou même obtenir un déclenchement d’exception dû à une impossibilité de décoder :

>>> with open('texte_iso8859_15.txt', 'r', encoding='utf_8') as entree:
        lu4 = entree.readline ()

...

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe8 in position 1: invalid continuation byte

L’attribut encoding

L’attribut encoding donne le codage/décodage utilisé par un canal ouvert vers un fichier texte.

>>> with open('texte_utf8.txt', 'r', encoding='utf_8') as entree:
       print(entree.encoding)
utf_8
>>> with open('texte_utf8.txt', 'r', encoding='iso8859_15') as entree:
       print(entree.encoding)
iso8859_15

Les canaux standards

Il existe trois canaux dits standards prédéfinis :

  • le canal standard de lecture : stdin ;

  • le canal standard d’écriture : stdout ;

  • et le canal standard d’erreur : stderr.

En Python, ces canaux sont des variables définies dans le module sys.

Nous ne dirons quelques mots que pour les deux premiers.

Canal standard de lecture

La variable stdin du module sys représente un canal de lecture en mode texte. L’encodage par défaut dépend du système sur lequel on l’utilise. Ce canal est ouvert, la commande open est donc inutile.

Voici ce que nous apprend l’interpréteur Python lorsqu’on l’interroge sur ce canal 1 :

>>> import sys
>>> sys.stdin
<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>

Depuis quel fichier les opérations de lecture via ce canal seront-elles effectuées ? La réponse sur un ordinateur classique est (par défaut) le clavier.

Avec la méthode readline, une fois la commande exécutée, l’interpréteur attend (patiemment) qu’une ligne de texte soit tapée au clavier avec bien entendu son marqueur de fin de ligne qui, au clavier, est la touche Entrée.

>>> sys.stdin.readline()
Une ligne de texte
'Une ligne de texte\n'

Dans ce qui précède, la commande sys.stdin.readline() attend de l’utilisateur qu’il tape une ligne (ici Une ligne de texte). Une fois cette ligne terminée, la commande renvoie la chaîne de caractères lue au clavier, accompagnée de son marqueur de fin de ligne.

La méthode readlines quant à elle est en mesure de lire un nombre quelconque de lignes. Seule la fin de fichier marque la fin de la lecture. Comment est marquée la fin de fichier pour une lecture de données au clavier ? Cela dépend du système sur lequel on travaille :

  • dans un système du type Unix : Ctrl + D

  • dans un système Windows : Ctrl + Z.

>>> sys.stdin.readlines()
une ligne
une autre ligne
et encore une autre.
['une ligne\n', 'une autre ligne\n', 'et encore une autre.\n']

C’est la même chose pour la méthode read sauf qu’elle renvoie une chaîne de caractères au lieu d’une liste de chaînes.

>>> sys.stdin.read()
une ligne
une autre ligne
et encore une autre.
'une ligne\nune autre ligne\net encore une autre.\n'

Canal standard d’écriture

La variable stdout du module sys représente un canal d’écriture en mode texte qu’il est inutile d’ouvrir.

Voici ce que nous apprend l’interpréteur Python lorsqu’on l’interroge sur ce canal :

>>> sys.stdout
<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>

Sur un ordinateur classique, les opérations d’écriture se font (par défaut) sur l’écran.

Les méthodes write et writelines permettent donc d’effectuer des opérations d’écriture de chaînes ou de listes de chaînes.

>>> sys.stdout.write('Timoleon est un homme politique')
Timoleon est un homme politique21
>>> sys.stdout.write ('Timoleon est un homme politique\n')
Timoleon est un homme politique
21
>>> sys.stdout.writelines(['Timoleon', ' est\n', ' un', ' homme', ' politique\n'])
Timoleon est
 un homme politique

La fonction input

La fonction input prédéfinie en Python permet de lire une chaîne de caractères tapée au clavier. Elle peut événtuellement être utilisée avec une chaîne de caractères passée en paramètre qui pourra jouer le rôle d’une invite à la saisie.

Voici comment on peut envisager de la programmer avec les deux canaux stdin et stdout :

def my_input(prompt=''):
    sys.stdout.write(prompt)
    return sys.stdin.readline()

En principe cette fonction contient tout ce qu’il faut pour jouer le même rôle que la fonction input : elle imprime le prompt sur la sortie standard, puis elle renvoie la ligne tapée par l’utilisateur.

Malheureusement, à l’usage un phénomène troublant apparaît :

>>> my_input('Votre nom ? ')
Timoleon
Votre nom ? 'Timoleon\n'

Le prompt n’est imprimé seulement qu’après l’entrée de son nom par l’utilisateur !

Cela est dû au fait que le système n’effectue pas toujours immédiatement les opérations d’écritures : il les temporise (buffer). Si on veut forcer une opération d’écriture temporisée, on peut utiliser la méthode flush.

Voici donc une nouvelle version de la fonction my_input qui remédie au défaut constaté :

def my_input(prompt=''):
    """
    :param prompt: (str) [optionnel, valeur par défaut: chaine vide] chaîne à imprimer avant lecture
    :return: (str) chaîne lue depuis l'entrée standard
    :effet de bord: interrompt le flux d'exécution pour lire une chaîne 
              de caractères depuis l'entrée standard.
    """
    sys.stdout.write(prompt)
    sys.stdout.flush()
    return sys.stdin.readline().rstrip('\n')
>>> my_input ('Votre nom ? ')
Votre nom ? Timoleon
'Timoleon'

Remarque

Notez l’utilisation de la méthode rstrip qui supprime d’une chaîne de caractères le caractère passé en paramètre situé à droite. Ici c’est le marqueur de fin de ligne située à la fin, de la chaîne renvoyée par readline qui est supprimé.

Traitement des données lues

Si les opérations d’écriture ne posent en général pas trop de problèmes, il n’en va pas de même pour les opérations de lecture.

Lorsque dans un programme des données sont lues, c’est pour qu’elles soient l’objet d’un certain traitement. Quelle certitude pouvons nous avoir sur la conformité ou validité de ces données, surtout lorsqu’elles proviennent d’une saisie par un opérateur humain ?

Retour sur les exceptions

Certaines instructions, bien que syntaxiquement correctes, peuvent déclencher pendant leur exécution des erreurs, erreurs qui interrompent cette exécution en déclenchant ce qu’en programmation on nomme exception.

En voici un petit panorama :

>>> 1 // 0
Traceback (most recent call last):
  ...
ZeroDivisionError: integer division or modulo by zero
>>> l = [0, 1]
>>> l[2]
Traceback (most recent call last):
  ...
IndexError: list index out of range
>>> int ('timoleon')
Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: 'timoleon'
>>> x = 0
>>> assert x != 0, 'x ne peut pas être nul'
Traceback (most recent call last):
  ...
AssertionError: x ne peut pas être nul

Le loup entre dans la bergerie

Prenons pour exemple, le cas de la saisie au clavier d’une date. Concevons donc une fonction sans paramètre dont le travail consiste à lire une date dans un format j/m/a, composé de trois nombres désignant le jour, le mois et l’année, séparés par des /.

La fonction qui suit donne une réalisation possible d’une telle fonction. Elle consiste

  • à saisir les données à l’aide de la fonction input ;

  • à découper la réponse à l’aide de la méthode split ;

  • et enfin à construire un triplet d’entiers avec les trois composantes de la réponse (qui sont des chaînes de caractères).

def lire_date1():
    """
    :return: (tuple) une date sous la forme d'un triplet 
             (j, m, a) de nombres entiers 
    :effet de bord: lecture d'une donnée sur l'entrée standard
    :CU: validité des données lues
    """
    reponse = input('j/m/a ? ')
    reponse = reponse.split('/')
    date = (int(reponse[0]), int(reponse[1]), int(reponse[2]))
    return date

Cette fonction convient bien pour des données correctement saisies :

>>> lire_date1()
j/m/a ? 22/2/2016
(22, 2, 2016)

Mais, elle accepte des données incorrectes comme

>>> lire_date1()
j/m/a ? 2/22/2016
(2, 22, 2016)

et déclenche des exceptions pour des données au format incorrect

>>> lire_date1()
j/m/a ? 22/fevrier/2016
Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: 'fevrier'
>>> lire_date1()
j/m/a ? 22-2-2016
Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: '22-2-2016'

Le loup est entré dans la bergerie !

Le loup est l’utilisateur du programme. Le programmeur ne le connaît le plus souvent pas. Et cet utilisateur est très certainement un être humain qui peut commettre des erreurs comme celles mentionnées ci-dessus.

L’instruction try ... except ou comment repousser le loup ?

L’instruction try: ... except... permet au programmeur de programmer des situations normales (partie try) et prévoir des situations anormales ou exceptionnelles (partie except).

Une telle forme d’instruction est appelée traitement d’exceptions. En Python elle s’écrit toujours :

try:
   # instructions du traitement normal
except <exception> :
   # instructions du traitement exceptionnel

Elle commence par le mot-clé try suivi des deux points (:), suivie d’un bloc (indenté) d’instructions à exécuter dans le cas où aucune exception n’est déclenchée. Elle est suivie par une (ou plusieurs) partie(s) débutant par le mot-clé except définissant les actions à réaliser dans le cas où l’exception précisée à la suite du mot-clé except est déclenchée.

En voici un exemple :

>>> x = 1
>>> try:
...     x = x + 10 // 0
... except ZeroDivisionError:
...     print('Tentative de division par zéro')

Tentative de division par zéro

Comme on le constate, la partie normale déclenche l’exception ZeroDivisionError, exception qui est traitée dans la partie qui suit par l’exécution d’une simple instruction print. La variable x n’a pas pu être modifiée.

>>> x
1

Considérons ce second exemple :

>>> x = 1
>>> try:
...     x = x + 10 // int('zero')
... except ZeroDivisionError:
...     print('Tentative de division par zéro')

Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: 'zero'

La fonction int est appliquée à une chaîne de caractères qui ne correspond pas à l’écriture décimale d’un nombre entier : elle déclenche donc une exception ValueError. Celle-ci n’est pas traitée puisque seule ZeroDivisionError l’est. Voici donc un traitement de deux exceptions :

>>> x = 1
>>> try:
...     x = x + 10 // int ('zero')
... except ZeroDivisionError:
...     print('Tentative de division par zéro')
... except ValueError:
...     print('Écriture du nombre incorrecte')

Écriture du nombre incorrecte

On peut si on veut rassembler les deux exceptions un seul cas :

>>> x = 1
>>> try:
...     x = x + 10 // int('zero')
... except ZeroDivisionError, ValueError:
...     print('Situation anormale')

Situation anormale

mais cela n’est pas toujours une très bonne idée, puisque le traitement à effectuer dans une situation anormale dépend souvent de l’exception déclenchée.

Vers une meilleure version de la fonction lire_date

La version lire_date2 contrôle la saisie en refusant toute saisie ne comportant pas deux caractères / séparant les trois nombres. Une boucle infinie (while True) répète inlassablement la saisie tant qu’elle ne contient pas les deux séparateurs requis. On sort de cette boucle infinie via l’instruction return.

def lire_date2():
    """
    :return: (tuple) une date sous la forme d'un triplet 
             (j, m, a) de nombres entiers 
    :effet de bord: lecture d'une donnée sur l'entrée standard
    :CU: validité des données lues
    """
    while True:
        reponse = input('j/m/a ? ')
        reponse = reponse.split('/')
        try:
            assert len(reponse) == 3, 'il faut 2 /'
            date = (int(reponse[0]), int(reponse[1]), int(reponse[2]))
            return date
        except AssertionError:
            print('il faut 2 / ')       

Voici un appel à cette fonction dans lequel il faut trois tentatives à l’utilisateur pour fournir une saisie avec deux / :

>>> lire_date2()
j/m/a ? 29-2-2016
j/m/a ? 29/2-2016
j/m/a ? 29/2/2016
(29, 2, 2016)

La fonction int étant assez souple avec les espaces entourant les nombres, la saisie qui suit se passe très bien :

>>> lire_date2()
j/m/a ?   29   /  2    / 2016
(29, 2, 2016)

La fonction lire_date2 assure que la saisie des informations de l’utilisateur comprend bien deux séparateurs /. Mais elle ne contrôle absolument pas le contenu des trois champs séparés qui pourraient très bien ne pas contenir de représentation de nombres entiers :

>>> lire_date2()
j/m/a ? a/b/c
Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: 'a'

La fonction lire_date3 récupère l’exeption ValueError :

def lire_date3():
    """
    :return: (tuple) une date sous la forme d'un triplet 
             (j, m, a) de nombres entiers 
    :effet de bord: lecture d'une donnée sur l'entrée standard
    :CU: validité des données lues
    """
    while True:
        reponse = input('j/m/a ? ')
        reponse = reponse.split('/')
        try:
            assert len(reponse) == 3, 'il faut 2 /'
            date = (int(reponse[0]), int(reponse[1]), int(reponse[2]))
            return date
        except AssertionError:
            print('il faut 2 /')
        except ValueError:
            print('Date exprimée avec trois nombres svp !')
>>> lire_date3()
j/m/a ? 29 2 2016
il faut 2 /
j/m/a ? 29 / fevrier / 2016
Date exprimée avec trois nombres svp !
j/m/a ? 29 / 2 / 2016
(29, 2, 2016)

Cette fonction ne contrôle pas la validité de la date saisie :

>>> lire_date3()
j/m/a ? 290/20/2016
(290, 20, 2016)

Voici une quatrième version de la fonction lire_date. Elle suppose défini un prédicat date_valide qui renvoie True ou False en fonction de la validité du triplet d’entiers passé en paramètre pour représenter une date.

À noter que l’exception AssertionError peut être déclenchée par deux instructions assert, et qu’il n’y a qu’une seule clause de traitement de cette exception. Le message imprimé à destination de l’utilisateur est mis dans une variable err.

def lire_date4():
    """
    :return: (tuple) une date sous la forme d'un triplet 
             (j, m, a) de nombres entiers 
    :effet de bord: lecture d'une donnée sur l'entrée standard
    :CU: validité des données lues
    """
    while True:
        reponse = input('j/m/a ? ')
        reponse = reponse.split('/')
        try:
            assert len(reponse) == 3, 'il faut 2 /'
            date = (int(reponse[0]), int(reponse[1]), int(reponse[2]))
            assert date_valide(date), 'date non valide'
            return date
        except AssertionError as err:
            print(err)
        except ValueError:      
            print('Date exprimée avec trois nombres svp !')
>>> lire_date4()
j/m/a ? 290 20 2016
il faut 2 /
j/m/a ? 290/20/2016
date non valide
j/m/a ? 29/2/2016
(29, 2, 2016)

Avertissement

La version ci-dessous de la fonction lire_date est un exemple à ne pas suivre parce que

  • elle ne distingue aucune exception (ligne except: non accompagnée du nom de l’exception)

  • elle inclut trop de code dans le traitement (lecture et découpage inclus dans le try:).

def lire_date5():
    """
    :return: (tuple) une date sous la forme d'un triplet 
             (j, m, a) de nombres entiers 
    :effet de bord: lecture d'une donnée sur l'entrée standard
    :CU: validité des données lues
    """
    while True:
        try:
            reponse = input('j/m/a ? ')
            reponse = reponse.split('/')
            assert len(reponse) == 3
            date = (int(reponse[0]), int(reponse[1]), int(reponse[2]))
            assert date_valide(date), 'date non valide'
            return date
        except:
            print('Date au format j/m/a svp !')

Une conséquence de l’absence de distinction des exceptions pouvant être déclenchées, fait que, une fois appelée, la fonction lire_date5 attend patiemment qu’une date valide soit tapée par l’utilisateur. Celui-ci ne peut pas interrompre l’exécution de cette fonction par la combinaison de touches Ctrl+C (exception KeyboardInterrupt), ni par le marqueur de fin de fichier Ctrl+D (exception EOFError), car chacune de ces exceptions est récupérée par la partie except:.

Notes

1

La réponse de Python sur la valeur de la variable sys.stdin dépend non seulement de la plateforme sur laquelle on travaille, mais aussi du logiciel utilisé. Ainsi la réponse mentionnée dans ce texte suppose qu’on utilise un simple interpréteur Python (python3 ou ipython3). Mais si on travaille avec Idle, on obtient la réponse :

>>> sys.stdin
<idlelib.PyShell.PseudoInputFile object at 0x7f90ed9d3630>

(Le nombre en hexa 0x... peut aussi ne pas être celui ci-dessus.)