IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Plongez au coeur de Python ,De débutant à expert


précédentsommairesuivant

VI. Traitement des exceptions et utilisation de fichiers

Dans ce chapitre vous plongerez dans les exceptions, les objets-fichiers, les boucles for et les modules os et sys. Si vous avez utilisé les exceptions dans un autre langage de programmation, vous pouvez survoler la première section pour avoir une idée de la syntaxe de Python. Assurez-vous de reprendre une lecture détaillée pour l'utilisation des fichiers.

VI-A. Traitement des exceptions

Comme beaucoup de langages orientés objet, Python gère les exception à l'aide de blocs try...except.

Python utilise try...except pour gérer les exceptions et raise pour les générer. Java et C++ utilisent try...catch pour gérer les exceptions et throw pour les générer.

Les exceptions sont partout en Python, pratiquement chaque module de la librairie standard Python les utilise et Python lui-même en déclenchera dans de nombreuses circonstances différentes. Vous les avez déjà vu à plusieurs reprises tout au long de ce livre.

  • Accéder à une clé non-existante d'un dictionnaire déclenche une exception KeyError.
  • Chercher une valeur non-existante dans une liste déclenche une exception ValueError.
  • Appeler une méthode non-existante déclenche une exception AttributeError.
  • Référencer une variable non-existante déclenche une exception NameError.
  • Mélanger les types de données sans conversion déclenche une exception TypeError.

Dans chacun de ces cas, nous ne faisions qu'expérimenter à l'aide de l'IDE Python : une erreur se produisait, l'exception était affichée (éventuellement, en fonction de votre IDE, dans un rouge détonnant) et c'était tout. C'est ce que l'on appelle une exception non-gérée, lorsque l'exception a été déclenchée, il n'y avait pas de code pour la prendre en charge explicitement, elle est donc remontée jusqu'à Python qui l'a traité selon la méthode par défaut, qui est d'afficher une information de débogage et d'abandonner. Dans l'IDE ce n'est pas un problème, mais si cela arrivait pendant le déroulement d'un de vos programmes Python réels, le programme dans son ensemble serait arrété.

Cependant, une exception ne doit pas forcément entrainer le plantage complet d'un programme. Les exceptions, lorsqu'elles sont déclenchées, peuvent être gérées. Parfois une exception se produit parcequ'il y a réellement un bogue dans votre code (comme tenter d'accéder à une variable qui n'existe pas), mais souvent, une exception est un évènement que vous pouvez prévoir. Si vous ouvrez un fichier, il peut ne pas exister, si vous vous connectez à une base de données, elle peut être indisponible, ou peut-être n'avez-vous pas les droits nécéssaires pour y accéder. Si vous savez qu'une ligne de code est susceptible de déclencher un exception, vous devriez gérer l'exception avec un bloc try...except.

Exemple 6.1. Ouverture d'un fichier inexistant

 
Sélectionnez
>>> fsock = open("/notthere", "r")      ***1***
	Traceback (innermost last):
  File "<interactive input>", line 1, in ?
	IOError: [Errno 2] No such file or directory: '/notthere'
	>>> try:
	...     fsock = open("/notthere")       ***2***
	... except IOError:                     ***3***
	...     print "The file does not exist, exiting gracefully"
	... print "This line will always print" ***4***
	The file does not exist, exiting gracefully
	This line will always print

***1*** En utilisant la fonction prédéfinie open, nous pouvons ouvrir un fichier en lecture (nous verrons open plus en détail dans la section suivante). Mais le fichier n'existe pas, ce qui déclenche une exception IOError. Comme nous n'avons pas fourni de gestionnaire pour l'exception IOError, Python se contente d'afficher des informations de débogage et abandonne.

***2*** Nous allons essayer d'ouvrir le même fichier non-existant, mais cette fois à l'intérieur d'un bloc try...except.

***3*** Quand la méthode open déclenche une exception IOError, nous sommes prêts. La ligne except IOError: intercepte l'exception et exécute notre propre bloc de code, qui en l'occurence ne fait qu'afficher un message d'erreur plus agréable.

***4*** Une fois qu'une exception a été traitée, le traitement continue normalement à la première ligne après le bloc try...except. Notez que cette ligne sera toujours affichée, qu'une exception se produise ou pas. Si vous aviez vraiment un fichier appelé notthere dans votre répertoire racine, l'appel a open réussirait, la clause except serait ignorée, mais cette ligne serait quand même exécutée.

Les exceptions peuvent sembler hostiles (après tout, si vous ne les interceptez pas, votre programme plante), mais réflechissez à l'alternative. Voudriez-vous plutôt un objet-fichier inutilisable pointant vers un fichier non-existant ? De toute manière, vous auriez quand même à vérifier sa validité, sinon votre programme produirait des erreurs bizarres plus loin dont vous auriez à retrouver la source. Je suis sûr que vous avez déjà fait cela, ce n'est pas drôle. Avec les exceptions, les erreurs se produisent immédiatement et vous pouvez les gérer de manière standardisée à la source du problème.

VI-A-1. Utilisation d'exceptions pour d'autres cas que la gestion d'erreur

Il y a de nombreux autres usages pour les exceptions en dehors de la prise en compte de véritables conditions d'erreurs. Un des usages commun dans la bibliothèque standard Python est d'essayer d'importer un module, puis de vérifier si cela à marché. Importer un module qui n'existe pas déclenchera une exception ImportError. Vous pouvez utiliser cela pour définir des niveaux multiples de fonctionnalité basé sur la disponibilité des modules à l'exécution, ou pour supporter plusieurs plateformes (dans ce cas le code spécifique à chaque plateforme est séparé dans différents modules).

Vous pouvez aussi définir vos propre exceptions en créant un classe qui hérite de la classe prédéfinie Exception et déclencher vos exceptions avec l'instruction raise. Cela dépasse le champ de cette section, voyez la section «Pour en savoir plus» si vous êtes intéressé.

L'exemple suivant démontre l'utilisation d'une exception pour le support d'une fonctionnalité spécifique à une plate-forme. Ce code vient du module getpass, un module enveloppe pour obtenir un mot de passe de l'utilisateur. Obtenir un mot de passe est fait de manière différente sous UNIX, Windows et Mac OS, mais ce code encapsule toutes ces différences.

Exemple 6.2. Support de fonctionnalités propre à une plate-forme
 
Sélectionnez
	  # Bind the name getpass to the appropriate function
	  try:
	      import termios, TERMIOS                     ***1***
	  except ImportError:
	      try:
		  import msvcrt                           ***2***
	      except ImportError:
		  try:
		      from EasyDialogs import AskPassword ***3***
		  except ImportError:
		      getpass = default_getpass           ***4***
		  else:                                   ***5***
		      getpass = AskPassword
	      else:
		  getpass = win_getpass
	  else:
	      getpass = unix_getpass

***1*** termios est un module spécifique à UNIX qui fournit un contrôle de bas niveau sur le terminal d'entrée. Si ce module n'est pas disponible (parcequ'il n'est pas sur votre système ou que votre système ne le supporte pas), l'import échoue et Python déclenche une exception ImportError, que nous interceptons.

***2*** OK, nous n'avons pas termios, essayons donc msvcrt, qui est un module spécifique à Windows qui fournit une API pour de nombreuses fonctions utiles des services d'exécution de Microsoft Visual C++. Si l'import échoue, Python déclenche une exception ImportError, que nous interceptons.

***3*** Si les deux premiers n'ont pas marché, nous essayons d'importer une fonction de EasyDialogs, qui est un module spécifique à Mac OS qui fournit des fonctions pour afficher des boîtes de dialogue de différents types. Encore une fois, si cette import échoue, Python déclenche une exception ImportError, que nous interceptons.

***4*** Aucun de ces modules spécifiques n'est disponible (ce qui est possible puisque Python a été porté sur de nombreuses plateformes), nous devons donc nous replier sur la fonction de saisie de mot de passe par défaut (qui est définie ailleurs dans le module getpass). Remarquez ce que nous faison là : nous assignons la fonction default_getpass à la variable getpass. Si vous lisez la documentation officielle de getpass, elle vous dit que le module getpass définit une fonction getpass. C'est comme ça qu'il le fait, en assignant getpass à la bonne fonction pour votre plateforme. Quand vous appelez ensuite la fonction getpass, vous appelez en fait une fonction spécifique à la plateforme que ce code a mis en place pour vous. Vous n'avez pas à vous soucier de la plateforme sur laquelle votre code est exécuté, appelez getpass, qui fera ce qu'il faut.

***5*** Un bloc try...except peut avoir une clause else, comme une instruction if. Si aucune exception n'est déclenchée dans le bloc try, la clause else est exécutée à la suite. Dans ce cas, cela veut dire que l'import from EasyDialogs import AskPassword a fonctionné et donc nous assignons getpass à la fonction AskPassword. Chacun des autres blocs try...except a une clause else similaire pour assigner getpass à la bonne fonction lorsque nous trouvons un import qui marche.

Pour en savoir plus sur le traitement des exceptions

VI-B. Les objets-fichier

Python a une fonction prédéfinie, open, pour ouvrir un fichier sur le disque. open retourne un objet-fichier qui possède des méthodes et des attributs pour obtenir des informations et manipuler le fichier ouvert.

Exemple 6.3. Ouverture d'un fichier

 
Sélectionnez
>>> f = open("/music/_singles/kairo.mp3", "rb") ***1***
	>>> f                                           ***2***
	<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.mode                                      ***3***
'rb'
>>> f.name                                      ***4***
'/music/_singles/kairo.mp3'

***1*** La méthode open peut prendre jusqu'à trois paramètres : un nom de fichier, un mode et un paramètre de tampon. Seul le premier, le nom de fichier, est nécéssaire, les deux autres sont optionnels. Si le mode n'est pas spécifié, le fichier est ouvert en mode texte pour la lecture. Ici nous ouvrons le fichier en mode binaire pour la lecture (print open.__doc__ affiche une bonne explication de tous les modes possibles).

***2*** La fonction open retourne un objet (arrivé à ce point cela ne doit pas vous surprendre). Un objet-fichier à plusieurs attributs utiles.

***3*** L'attribut mode d'un objet-fichier vous indique dans quel mode le fichier a été ouvert.

***4*** L'attribut name d'un objet-fichier vous indique le nom du fichier qui a été ouvert.

VI-B-1. Lecture d'un fichier

Une fois un fichier ouvert, la première chose que l'on peut faire est de le lire, comme nous allons le voir dans l'exemple suivant.

Exemple 6.4. Lecture d'un fichier
 
Sélectionnez
>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.tell()              ***1***
0
>>> f.seek(-128, 2)       ***2***
>>> f.tell()              ***3***
7542909
>>> tagData = f.read(128) ***4***
>>> tagData
'TAGKAIRO****THE BEST GOA         ***DJ MARY-JANE***            
Rave Mix                      2000http://mp3.com/DJMARYJANE     \037'
>>> f.tell()              ***5***
7543037

***1*** Un objet-fichier maintien des informations d'état sur le fichier qui est ouvert. La méthode tell d'un objet-fichier vous indique la position actuelle dans le fichier ouvert. Comme nous n'avons encore rien fait de ce fichier la position actuelle est 0, le début du fichier. ***2*** La méthode seek d'un objet-fichier permet de se déplacer dans le fichier ouvert. Le deuxième paramètre précise ce que le premier signifie : 0 pour un déplacement à une position absolue (en partant du début du fichier), 1 pour une position relative (en partant de la position actuelle) et 2 pour une position relative à la fin du fichier. Puisque les balises MP3 que nous recherchons sont stockés à la fin du fichier, nous utilisons 2 et nous déplaçons à 128 octets de la fin du fichier. ***3*** La méthode tell confirme que la position actuelle a changé. ***4*** La méthode read lit un nombre d'octets spécifié du fichier ouvert et retourne une chaîne contenant les données lues. Le paramètre optionnel précise le nombre maximal d'octets à lire. Si aucun paramètre n'est spécifié, read lit jusqu'à la fin du fichier. (Nous aurions pu taper simplement read() ici, puisque nous savons exactement où nous sommes dans le fichier et que nous lisons en fait les 128 derniers octets.) Les données lues sont assignées à la variable tagData et la position actuelle est mise à jour en fonction du nombre d'octets lus. ***5*** La méthode tell confirme que la position actuelle a changé. Si vous faites le calcul, vous verrez qu'après que nous ayons lu 128 octets, la position a été incrémenté de 128.

VI-B-2. Fermeture d'un fichier

Les fichiers ouverts consomment des ressources système et, en fonction du mode d'ouverture, peuvent ne pas être accessibles à d'autres programmes. Il est donc important de fermer les fichiers dès que vous ne les utilisez plus.

Exemple 6.5. Fermeture d'un fichier
 
Sélectionnez
>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed       ***1***
False
>>> f.close()      ***2***
>>> f
<closed file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed       ***3***
True
>>> f.seek(0)      ***4***
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.tell()
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.read()
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.close()      ***5***

***1*** L'attribut closed d'un objet-fichier indique si l'objet pointe un fichier ouvert ou non. Dans ce cas, le fichier est toujours ouvert (closed vaut False).

***2*** Pour fermer un fichier, appelez la méthode close de l'objet-fichier. Cela libère le verrou (s'il existe) que vous avez sur le fichier, purge les tampons en écriture (s'ils existent) et libère les ressources système.

***3*** L'attribut closed confirme que le fichier est fermé.

***4*** Ce n'est pas parce que le fichier est fermé que l'objet fichier cesse d'exister. La variable f continuera d'exister jusqu'à ce qu'elle soit hors de portée ou qu'elle soit supprimée manuellement. Cependant aucune des méthodes de manipulation d'un fichier ouvert ne marchera après que le fichier ait été fermé, elles déclencheront toutes une exception.

***5*** Appeler close sur un objet-fichier dont le fichier est déjà fermé ne déclenche pas d'exception mais échoue silencieusement.

VI-B-3. Gestion des erreurs d'entrée/sortie

Maintenant vous en avez vu assez pour comprendre le code de gestion de fichier dans le programme d'exemple fileinfo.py du chapitre précédent. L'exemple suivant montre comment ouvrir et lire un fichier de manière sûre en gérant les erreurs.

Exemple 6.6. Les objets-fichier dans MP3FileInfo
 
Sélectionnez
       try:                                ***1***
            fsock = open(filename, "rb", 0) ***2***
            try:                           
                fsock.seek(-128, 2)         ***3***
                tagdata = fsock.read(128)   ***4***
            finally:                        ***5***
                fsock.close()              
            .
            .
            .
        except IOError:                     ***6***
            pass

***1*** Comme l'ouverture et la lecture de fichiers est risquée et peut déclencher une exception, tout ce code est enveloppé dans un bloc try...except (alors, l'indentation standardisée n'est-elle pas admirable ? C'est là que l'on commence a vraiment l'apprécier).

***2*** La fonction open peut déclencher une exception IOError (peut-être que le fichier n'existe pas).

***3*** La méthode seek peut déclencher une exception IOError (peut-être que le fichier fait moins de 128 octets).

***4*** La méthode read peut déclencher une exception IOError (peut-être que le disque a un secteur défectueux ou le fichier est sur le réseau et le réseau est en rideau).

***5*** Voilà qui est nouveau : un bloc try...finally. Une fois le fichier ouvert avec succès par la fonction open, nous voulons être absolument sûrs que nous le refermons, même si une exception est déclenchée par les méthodes seek ou read. C'est à cela que sert un bloc try...finally : le code du bloc finally sera toujours exécuté, même si une exception est déclenchée dans le bloc try. Pensez-y comme à du code qui est exécuté «au retour», quoi qu'il se soit passé «en route».

***6*** Enfin, nous gérons notre exception IOError. Cela peut être l'exception IOError déclenchée par l'appel à open, seek, ou read. Ici, nous ne nous en soucions vraiment pas car tout ce que nous faisons et d'ignorer l'erreur et de continuer (rappelez-vous que pass est une instruction Python qui ne fait rien). C'est tout à fait légal, «gérer» une exception peut vouloir dire explicitement ne rien faire. Cela compte quand même comme une exception gérée et le traitement va reprendre normalement à la prochaine ligne de code après le bloc ...except.

VI-B-4. Ecriture dans un fichier

Nous pouvons bien sûr écrire dans un fichier, cela se fait en grande partie de la même manière que pour la lecture. Il y a deux modes d'ouverture de base :

  • Le mode Append (ajout) pour ajouter des données à la fin du fichier.
  • Le mode Write (écriture) pour écraser le contenu du fichier.

Les deux modes créeront le fichier automatiquement s'il n'existe pas encore, il n'y a donc pas besoin de logique du type «si le fichier de journalisation n'existe pas, créer un nouveau fichier vide et l'ouvrir en écriture.» Il suffit d'ouvrir le fichier pour commencer à y écrire.

Exemple 6.7. Ecriture dans un fichier
 
Sélectionnez
>>> logfile = open('test.log', 'w') ***1***
>>> logfile.write('test succeeded') ***2***
>>> logfile.close()
>>> print file('test.log').read()   ***3***
test succeeded
>>> logfile = open('test.log', 'a') ***4***
>>> logfile.write('line 2')
>>> logfile.close()
>>> print file('test.log').read()   ***5***
test succeededline 2

***1*** Nous commençons par créer un nouveau fichier test.log ou l'écraser s'il existe et l'ouvrir en écriture (le second paramètre "w" signifie ouverture en écriture). Effectivement, c'est aussi dangeureux que ça en a l'air. J'espère que vous n'aviez rien de précieux dans ce fichier, parce que maintenant c'est effacé.

***2*** Vous pouvez écrire dans le fichier ouvert à l'aide de la méthode write de l'objet-fichier retourné par open.

***3*** file est un synonyme de open. Ici, nous ouvrons le fichier, lisons son contenu et l'imprimons en une seule ligne.

***4*** Nous savons que test.log existe (puisque nous venons juste d'écrire dedans), donc nous pouvons l'ouvrir pour ajouter des données (le paramètre "a" signifie ouverture pour ajout). En fait, nous pourrions le faire même si le fichier n'existait pas, puisque l'ouverture du fichier en mode ajout crée le fichier si nécéssaire. Mais le mode ajout n'endommagera jamais le contenu du fichier.

***5*** Comme vous pouvez le voir, la ligne d'origine aussi bien que la nouvelle ligne ajoutée sont maintenant dans test.log. Notez également que le retour à la ligne n'est pas inclus. Puisque nous n'en avons pas explicitement écrit dans le fichier, le fichier n'en contient pas. Nous pouvons écrire un retour à la ligne avec le caractère "\n". Puisque nous ne l'avons pas fait, tout ce que nous avons écrit finit sur la même ligne.

Pour en savoir plus sur l'utilisation de fichiers

VI-C. Itérations avec des boucles for

Comme la plupart des langages, Python a des boucles for. La seule raison pour laquelle vous ne les avez pas vues jusqu'à maintenant est que Python sait faire tellement d'autre choses que vous n'en avez pas besoin aussi souvent.

La plupart des autres langages n'ont pas de type de données liste aussi puissant que celui de Python, vous êtes donc amené à faire beaucoup de travail à la main, spécifier un début, une fin et un pas pour définir une suite d'entiers ou de caractères ou d'autres entités énumérables. Mais en Python une boucle for parcourt simplement une liste, de la même manière que les list comprehensions fonctionnent.

Exemple 6.8. Présentation des boucles for

 
Sélectionnez
>>> li = ['a', 'b', 'e']
>>> for s in li:         ***1***
...     print s          ***2***
a
b
e
>>> print "\n".join(li)  ***3***
a
b
e

***1*** La syntaxe d'une boucle for est similaire aux list comprehensions. li est une liste et s prend successivement la valeur de chaque élément, en commençant par le premier.

***2*** Comme une instruction if ou n'importe quel autre bloc indenté, une boucle for peut contenir autant de lignes de codes que vous le voulez.

***3*** Voici la raison pour laquelle vous n'aviez pas encore vu la boucle for : nous n'en avions pas eu besoin. C'est incroyable la fréquence à laquelle nous utilisons les boucles for dans d'autres langages alors que ce que nous voudrions vraiment et un join ou une list comprehension.

Faire un compteur «normal» (selon les critères de Visual Basic) pour la boucle for est également simple.

Exemple 6.9. Compteurs simples

 
Sélectionnez
>>> for i in range(5):             ***1***
...     print i
0
1
2
3
4
>>> li = ['a', 'b', 'c', 'd', 'e']
>>> for i in range(len(li)):       ***2***
...     print li[i]
a
b
c
d
e

***1*** Comme nous l'avons vu dans l'Exemple 3.20, «Assignation de valeurs consécutives», range produit une liste d'entiers que nous pouvons parcourir. Je sais que ça peut sembler étrange, mais c'est parfois (et j'insiste sur le parfois) utile d'avoir une boucle sur un compteur.

***2*** Ne faites jamais ça. C'est un style de pensée Visual Basic. Libérez-vous en. Parcourez simplement la liste comme dans l'exemple précédent.

Les boucles for ne sont seulement faites pour les compteurs simples. Elles peuvent parcourir de nombreuses choses. Voici un exemple d'utilisation d'une boucle for pour parcourir un dictionnaire.

Exemple 6.10. Parcourir un dictionnaire

 
Sélectionnez
>>> import os
>>> for k, v in os.environ.items():      ***1*** ***2***
...     print "%s=%s" % (k, v)
USERPROFILE=C:\Documents and Settings\mpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim

[...snip...]
>>> print "\n".join(["%s=%s" % (k, v)
...     for k, v in os.environ.items()]) ***3***
USERPROFILE=C:\Documents and Settings\mpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim

[...snip...]

***1*** os.environ est un dictionnaire des variables d'environnement définies dans votre système. Sous Windows ce sont vos variables utilisateur et système. Sous UNIX ce sont les variables exportées par le script de démarrage de votre shell. Sous Mac OS il n'y a pas de notion de variables d'environnement, ce dictionnaire est donc vide.

***2*** os.environ.items() retourne une liste de tuples : [(key1, value1), (key2, value2), ...]. La boucle for parcourt cette liste. A la première itération, il assigne key1 à k et value1 à v, donc k = USERPROFILE et v = C:\Documents and Settings\mpilgrim. A la seconde, k reçoit la deuxième clé, OS et v la valeur correspondante, Windows_NT.

***3*** Avec l'assignement multiple de variable et les list comprehensions, vous pouvez entièrement remplacer la boucle for par une seule instruction. Le choix d'une des deux formes dans votre code est une question de style personnel. J'aime ce style parce qu'il rend clair que ce que nous faisons est une mutation d'un dictionnaire en une liste, puis de joindre cette liste en une chaîne unique. D'autres programmeurs préfèrent la forme de la boucle for. Notez que la sortie est la même dans les deux cas, bien que cette version-ci soit légèrement plus rapide car il n'y a qu'une instruction print au lieu d'une par itération.

Maintenant nous pouvons examiner l'usage de la boucle for dans la classe MP3FileInfo du programme d'exemple fileinfo.py présenté au Chapitre 5.

Exemple 6.11. Boucle for dans MP3FileInfo

 
Sélectionnez
tagDataMap = {"title"   : (  3,  33, stripnulls),
                  "artist"  : ( 33,  63, stripnulls),
                  "album"   : ( 63,  93, stripnulls),
                  "year"    : ( 93,  97, stripnulls),
                  "comment" : ( 97, 126, stripnulls),
                  "genre"   : (127, 128, ord)}                               ***1***
    .
    .
    .
            if tagdata[:3] == "TAG":
                for tag, (start, end, parseFunc) in self.tagDataMap.items(): ***2***
                    self[tag] = parseFunc(tagdata[start:end])                ***3***

***1*** tagDataMap est un attribut de classe qui définit les balises que nous recherchons dans un fichier MP3 file. Les balises sont stockées dans des champ de longueur fixe, une fois que nous avons lu les derniers 128 octets du fichier, les octets 3 à 32 contiennent toujours le titre de la chanson, 33-62 le nom de l'artiste, 63-92 le nom de l'album etc. Notez que tagDataMap est un dictionnaire de tuples et que chaque tuple contient deux entiers et une référence de fonction.

***2*** Ceci à l'air compliqué, mais ne l'est pas. La structure des variables de for correspond à la structure des éléments de la liste retournée par items. Rappelez-vous, items retourne une liste de tuples de la forme (key, value). Le premier élément de cette liste est ("title", (3, 33, <function stripnulls>)), donc à la première itération de la boucle tag reçoit "title", start reçoit 3, end reçoit 33 et parseFunc reçoit la fonction stripnulls.

***3*** Maintenant que nous avons extrait tous les paramètres pour une balise MP3 unique, sauvegarder les données de la balise data est simple. Nous découpons tagdata de start à end pour obtenir les véritables données de cette balise, nous appelons parseFunc pour le traitement final des données et assignons le résultat comme valeur de la clé tag dans le pseudo-dictionnaire self. Après itération de tous les éléments de tagDataMap, self a les valeurs de toutes les balises et vous savez à quoi ça ressemble.

VI-D. Utilisation de sys.modules

Les modules, comme tout le reste en Python, sont des objets. Une fois qu'il a été importé, vous pouvez toujours obtenir une référence à un module à travers le dictionnaire global sys.modules.

Exemple 6.12. Présentation de sys.modules

 
Sélectionnez
>>> import sys                          ***1***
>>> print '\n'.join(sys.modules.keys()) ***2***
win32api
os.path
os
exceptions
__main__
ntpath
nt
sys
__builtin__
site
signal
UserDict
stat

***1*** Le module sys contient des informations système, comme la version de Python que vous utilisez (sys.version ou sys.version_info) et des options système comme la profondeur maximale de récursion autorisée (sys.getrecursionlimit() et sys.setrecursionlimit()).

***2*** sys.modules est un dictionnaire qui contient tous les modules qui ont été importés depuis que Python a été démarré. La clé est le nom de module, la valeur est l'objet module. Notez que cela comprend plus que les modules que votre programme a importé. Python charge certains modules au démarrage et si vous êtes dans une IDE Python, sys.modules contient tous les modules importés par tous les programmes que vous avez exécutés dans l'IDE.

Cet exemple présente l'utilisation de sys.modules.

Exemple 6.13. Utilisation de sys.modules

 
Sélectionnez
>>> import fileinfo         ***1***
>>> print '\n'.join(sys.modules.keys())
win32api
os.path
os
fileinfo
exceptions
__main__
ntpath
nt
sys
__builtin__
site
signal
UserDict
stat
>>> fileinfo
<module 'fileinfo' from 'fileinfo.pyc'>
>>> sys.modules["fileinfo"] ***2***
<module 'fileinfo' from 'fileinfo.pyc'>

***1*** Au fur et à mesure que des nouveaux modules sont importés, ils sont ajoutés à sys.modules. Cela explique pourquoi importer le même module deux fois est très rapide : Python a déjà chargé et mis en cache le module dans sys.modules, donc l'importer une deuxième fois n'est qu'une simple consultation de dictionnaire.

***2*** A partir du nom (sous forme de chaîne) de n'importe quel module déjà importé, vous pouvez obtenir une référence au module lui-même du dictionnaire sys.modules.

L'exemple suivant montre l'utilisation de l'attribut de classe __module__ avec le dictionnaire sys.modules pour obtenir une référence vers le module dans lequel la classe est définie.

Exemple 6.14. L'attribut de classe __module__

 
Sélectionnez
>>> from fileinfo import MP3FileInfo
>>> MP3FileInfo.__module__              ***1***
'fileinfo'
>>> sys.modules[MP3FileInfo.__module__] ***2***
<module 'fileinfo' from 'fileinfo.pyc'>

***1*** Chaque classe Python a un attribut de classe __module__ prédéfini, dont la valeur est le nom du module dans lequel la classe est définie.

***2*** En combinant cela au dictionnaire sys.modules vous pouvez obtenir une référence au module dans lequel la classe est définie.

Maintenant vous pouvez comprendre l'utilisation de sys.modules dans fileinfo.py, le programme d'exemple présenté au Chapitre 5. Cet exemple montre cette partie du code.

Exemple 6.15. sys.modules dans fileinfo.py

 
Sélectionnez
  def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):       ***1***
        "get file info class from filename extension"                             
        subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]        ***2***
        return hasattr(module, subclass) and getattr(module, subclass) or FileInfo ***3***

***1*** Ceci est une fonction avec deux arguments, filename est obligatoire, mais module est optionnel et est par défaut le module qui contient la classe FileInfo. Cela peut sembler peu efficace si on pense que Python évalue l'expression sys.modules à chaque fois que la fonction est appelée. En fait, Python n'évalue les expressions par défaut qu'une fois, la première fois que le module est importé. Comme nous le verrons plus loin nous n'appelons jamais cette fonction avec un argument module, module sert donc de constante au niveau de la fonction.

***2*** Nous détaillerons cette ligne plus tard, après avoir étudié le module os. Pour l'instant retenez simplement que subclass obtient le nom d'un classe, comme MP3FileInfo.

***3*** Vous connaissez déjà getattr, qui obtient une référence a un objet par son nom. hasattr est une fonction complémentaire qui vérifie si un objet possède un attribut particulier. Dans le cas présent, si un module possède une classe particulière (bien que cela fonctionne pour tout objet et tout attribut, tout comme getattr). En français, cette ligne de code dit «si le module a la classe nommée par subclass alors la retourner, sinon retourner la classe de base FileInfo».

Pour en savoir plus

VI-E. Travailler avec des répertoires

Le module os.path a de nombreuses fonctions pour manipuler les chemins de fichiers et de répertoires. Ici nous voulons gérer les chemins et lister le contenu d'un répertoire.

Exemple 6.16. Construction de noms de chemins

 
Sélectionnez
>>> import os
>>> os.path.join("c:\\music\\ap\\", "mahadeva.mp3") ***1*** ***2***
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.join("c:\\music\\ap", "mahadeva.mp3")   ***3***
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.expanduser("~")                         ***4***
'c:\\Documents and Settings\\mpilgrim\\My Documents'
>>> os.path.join(os.path.expanduser("~"), "Python") ***5***
'c:\\Documents and Settings\\mpilgrim\\My Documents\\Python'

***1*** os.path est une référence à un module, quel module exactement dépend de la plateforme que vous utilisez. Tout comme getpass encapsule les différences entre plateforme en assignant à getpass une fonction spécifique à la plateforme, os encapsule les différences entre plateformes en assignant à path un module spécifique à la plateforme.

***2*** La fonction join de os.path construit un nom de chemin à partir d'un ou de plusieurs noms de chemins partiels. Dans ce cas simple il ne fait que concaténer des chaînes (notez que traiter des noms de chemins sous Windows est ennuyeux car le backslash force à utiliser le caractère d'échappement).

***3*** Dans cette exemple un peu moins simple, join ajoute un backslash supplémentaire au nom de chemin avant de le joindre au nom de fichier. J'étais ravi quand j'ai découvert cela car addSlashIfNecessary est une des petites fonctions stupides que je dois toujours écrire quand je construis ma boîte à outil dans un nouveau langage. N'écrivez pas cette petite fonction stupide en Python, des gens intelligents l'ont déjà fait pour vous.

***4*** expanduser développe un nom de chemin qui utilise ~ pour représenter le répertoire de l'utilisateur. Cela fonctionne sur toutes les plateformes où les utilisateurs ont un répertoire propre comme Windows, UNIX et Mac OS X, c'est sans effet sous Mac OS.

***5*** En combinant ces techniques, vous pouvez facilement construire des noms de chemins pour les répertoires et les fichiers contenus dans le répertoire utilisateur.

Exemple 6.17. Division de noms de chemins

 
Sélectionnez
>>> os.path.split("c:\\music\\ap\\mahadeva.mp3")                        ***1***
('c:\\music\\ap', 'mahadeva.mp3')
>>> (filepath, filename) = os.path.split("c:\\music\\ap\\mahadeva.mp3") ***2***
>>> filepath                                                            ***3***
'c:\\music\\ap'
>>> filename                                                            ***4***
'mahadeva.mp3'
>>> (shortname, extension) = os.path.splitext(filename)                 ***5***
>>> shortname
'mahadeva'
>>> extension
'.mp3'

***1*** La fonction split divise un nom de chemin complet et retourne un tuple contenant le chemin et le nom de fichier. Vous vous rappelez quand je vous ai dit que vous pouviez utiliser l'assignement multiple de variables pour retourner des valeurs multiples d'une fonction ? Et bien split est une de ces fonctions.

***2*** Nous assignons la valeur de retour de la fonction split à un tuple de deux variables. Chaque variable reçoit la valeur de l'élément correspondant du tuple retourné.

***3*** La première variable, filepath, reçoit la valeur du premier élément du tuple retourné par split, le chemin du fichier.

***4*** La seconde variable, filename, reçoit la valeur du second élément du tuple retourné par split, le nom de fichier.

***5*** os.path contient aussi une fonction splitext, qui divise un nom de fichier et retourne un tuple contenant le nom de fichier et l'extension. Nous utilisons la même technique pour assigner chacun d'entre eux à des variables séparées.

Exemple 6.18. Liste des fichiers d'un répertoire

 
Sélectionnez
>>> os.listdir("c:\\music\\_singles\\")              ***1***
['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3', 
'spinning.mp3']
>>> dirname = "c:\\"
>>> os.listdir(dirname)                              ***2***
['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'cygwin',
'docbook', 'Documents and Settings', 'Incoming', 'Inetpub', 'IO.SYS',
'MSDOS.SYS', 'Music', 'NTDETECT.COM', 'ntldr', 'pagefile.sys',
'Program Files', 'Python20', 'RECYCLER',
'System Volume Information', 'TEMP', 'WINNT']
>>> [f for f in os.listdir(dirname)
...     if os.path.isfile(os.path.join(dirname, f))] ***3***
['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'IO.SYS', 'MSDOS.SYS',
'NTDETECT.COM', 'ntldr', 'pagefile.sys']
>>> [f for f in os.listdir(dirname)
...     if os.path.isdir(os.path.join(dirname, f))]  ***4***
['cygwin', 'docbook', 'Documents and Settings', 'Incoming',
'Inetpub', 'Music', 'Program Files', 'Python20', 'RECYCLER',
'System Volume Information', 'TEMP', 'WINNT']

***1*** La fonction listdir prend un nom de chemin et retourne une liste du contenu du répertoire.

***2*** listdir retourne à la fois les fichiers et les répertoires, sans indiquer lequel est quoi.

***3*** Vous pouvez utiliser le filtrage de liste et la fonction isfile du module os.path pour séparer les fichiers des répertoires. isfile prend un nom de chemin et retourne 1 si le chemin représente un fichier et 0 dans le cas contraire. Ici, nous utilisons os.path.join pour nous assurer que nous avons un nom de chemin complet, mais isfile marche aussi avec des chemins partiels, relatifs au répertoire en cours. Vous pouvez utiliser os.getcwd() pour obtenir le répertoire en cours.

***4*** os.path a aussi une fonction isdir qui retourne 1 si le chemin représente un répertoire et 0 dans le cas contraire. Vous pouvez l'utiliser pour obtenir une liste des sous-répertoires d'un répertoire.

Exemple 6.19. Liste des fichiers d'un répertoire dans fileinfo.py

 
Sélectionnez
def listDirectory(directory, fileExtList):                                        
    "get list of file info objects for files of particular extensions" 
    fileList = [os.path.normcase(f)
                for f in os.listdir(directory)]            ***1*** ***2***
    fileList = [os.path.join(directory, f) 
               for f in fileList
                if os.path.splitext(f)[1] in fileExtList]  ***3*** ***4*** ***5***

***1*** os.listdir(directory) retourne une liste de tous les fichiers et répertoires de directory.

***2*** En parcourant la liste avec f, nous utilisons os.path.normcase(f) pour normaliser la casse en fonction des paramètres par défaut du système d'exploitation. normcase est une petite fonction utile qui compense le problème des systèmes d'exploitation insensibles à la casse qui pensent que mahadeva.mp3 et mahadeva.MP3 sont le même fichier. Par exemple, sous Windows et Mac OS, normcase convertit l'ensemble du nom de fichier en minuscules, sous les systèmes compatibles UNIX, elle retourne le nom de fichier inchangé.

***3*** En parcourant la liste normalisée avec f à nouveau, nous utilisons os.path.splitext(f) pour diviser chaque nom de fichier en nom et extension.

***4*** Pour chaque fichier, nous regardons si l'extension est dans la liste d'extensions de fichier qui nous intéressent (fileExtList, qui a été passé à la fonction listDirectory).

***5*** Pour chaque fichier qui nous intéresse, nous utilisons os.path.join(directory, f) pour construire le chemin de fichier complet. Nous retournons une liste de noms de chemin complets.

A chaque fois que c'est possible, vous devriez utiliser les fonction de os et os.path pour les manipulations de fichier, de répertoire et de chemin. Ces modules enveloppent des modules spécifiques aux plateformes, les fonctions comme os.path.split marchent donc sous UNIX, Windows, Mac OS et toute autre plateforme supportée par Python.

Il existe une autre manière d'obtenir le contenu d'un répertoire. Elle est très puissante et utilise le type de jokers qui vous sont familier si vous utilisez la ligne de commande.

Exemple 6.20. Liste du contenu d'un répertoire avec glob

 
Sélectionnez
>>> os.listdir("c:\\music\\_singles\\")               ***1***
['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3',
'spinning.mp3']
>>> import glob
>>> glob.glob('c:\\music\\_singles\\*.mp3')           ***2***
['c:\\music\\_singles\\a_time_long_forgotten_con.mp3',
'c:\\music\\_singles\\hellraiser.mp3',
'c:\\music\\_singles\\kairo.mp3',
'c:\\music\\_singles\\long_way_home1.mp3',
'c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\_singles\\s*.mp3')          ***3***
['c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\*\\*.mp3')                  ***4***

***1*** Comme nous l'avons vu plus haut, os.listdir prend simplement un chemin de répertoire et retourne la liste de tous les fichiers et répertoires qu'il contient.

***2*** Le module glob, par contre, prend un joker et retourne le chemin complet de tous les fichiers et répertoires qui lui correspondent. Ici, le joker est un chemin de répertoire plus "*.mp3", c'est à dire tous les fichiers .mp3. Notez que chaque élément de la liste retournée contient le chemin complet du fichier.

***3*** Voici le joker pour trouver tous les fichiers d'un répertoire qui commencent par "s" et finissent par ".mp3".

***4*** Maintenant considerez le scénario suivant : vous avez un répertoire music, contenant plusieurs sous-répertoires, avec des fichiers .mp3 dans chaque sous-répertoire. Vous pouvez obtenir une liste de tous ces fichiers avec un seul appel à glob, en utilisant deux jokers à la fois. Un des jokers est "*.mp3" (qui correspond aux fichiers .mp3) et l'autre est à l'intérieur du chemin de répertoire, ce qui correspond aux sous-répertoires de c:\music. C'est une énorme puissance contenue dans une fonction à l'air faussement simple !

Pour en savoir plus sur le module os

VI-F. Assembler les pièces

A nouveau, tous les dominos sont en place. Nous avons vu comment chaque ligne de code fonctionne. Maintenant prenons un peut de recul pour voir comment tout cela s'assemble.

Exemple 6.21. listDirectory

 
Sélectionnez
def listDirectory(directory, fileExtList):                                         ***1***
    "get list of file info objects for files of particular extensions"
    fileList = [os.path.normcase(f)
                for f in os.listdir(directory)]           
    fileList = [os.path.join(directory, f) 
               for f in fileList
                if os.path.splitext(f)[1] in fileExtList]                          ***2***
    def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):       ***3***
        "get file info class from filename extension"                             
        subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]        ***4***
        return hasattr(module, subclass) and getattr(module, subclass) or FileInfo ***5***
    return [getFileInfoClass(f)(f) for f in fileList]                              ***6***

***1*** listDirectory est l'attraction principale de ce module. Elle prend un répertoire (c:\music\_singles\ dans mon cas) et une liste d'extensions intéressantes (comme ['.mp3']) et elle retourne une liste d'instances de classe qui se comportent comme des dictionnaires et qui contiennent des métadonnées concernant chaque fichier intéressant de ce répertoire. Et elle le fait en une poignée de ligne simples et directes.

***2*** Comme nous l'avons vu dans la section précédente, cette ligne de code permet d'obtenir une liste de noms de chemin complets de tous les fichiers de directory qui ont une extension de fichier intéressante (comme spécifiée par fileExtList).

***3*** Les programmeurs Pascal à l'ancienne les connaissent bien, mais la plupart des gens me jettent un regard vide quand je leur dit que Python supporte les fonctions imbriquées -- littéralement une fonction à l'intérieur d'une fonction. La fonction imbriquée getFileInfoClass peut seulement être appelée de la fonction dans laquelle elle est définie, listDirectory. Comme pour toute autre fonction, vous n'avez pas besoin d'une déclaration d'interface ou de quoi que ce soit d'autre, définissez juste la fonction et écrivez-la.

***4*** Maintenant que vous avez vu le module os, cette ligne devrait être plus compréhensible. Elle obtient l'extension du fichier (os.path.splitext(filename)[1]), la force en majuscules (.upper()), découpe le point ([1:]) et construit un nom de classe en formatant la chaîne. Donc, c:\music\ap\mahadeva.mp3 devient .mp3, puis .MP3, puis MP3 et enfin MP3FileInfo.

***5*** Ayant construit le nom de la classe qui doit manipuler ce fichier, nous vérifions si cette classe existe dans ce module. Si c'est le cas, nous retournons la classe, sinon, nous retournons la classe de base, FileInfo. C'est un point très important : cette fonction retourne une classe. Pas une instance de classe, mais la classe elle-même.

***6*** Pour chaque fichier dans notre liste de «fichiers intéressants» (fileList), nous appelons getFileInfoClass avec le nom de fichier (f). Appeler getFileInfoClass(f) retourne une classe, nous ne savons pas exactement laquelle mais cela ne nous intéresse pas. Nous créons alors une instance de cette classe (quelle qu'elle soit) et passons le nom du fichier (encore f), à la méthode __init__. Comme nous l'avons vu auparavant dans ce chapitre, la méthode __init__ de FileInfo définit self["name"], ce qui déclenche __setitem__, qui est redéfini dans la classe descendante (MP3FileInfo) comme une fonction traitant le fichier de manière à en extraire les métadonnées. Nous faisons cela pour tous les fichiers intéressants et retournons une liste des instances ainsi créées.

Notez que listDirectory est complètement générique. Il ne sait pas à l'avance quels types de fichiers iI va obtenir, ou quelles sont les classes qui pourraient triter ces fichiers. Il inspecte le répertoire à la recherche de fichiers à traiter, puis recourt à l'introspection sur son propre module pour voir quelles classes de traitement (comme MP3FileInfo) sont définies. Vous pouvez étendre ce programme pour gérer d'autres types de fichiers simplement en définissant une classe portant un nom approprié : HTMLFileInfo pour les fichiers HTML, DOCFileInfo pour les fichiers .doc de Word, etc. listDirectory les prendra tous en charge, sans modification, en se déchargeant du traitement proprement dit sur les classes appropriées et en assemblant les résultats.

VI-G. Résumé

Le programme fileinfo.py, introduit au Chapitre 5; devrait maintenant être parfaitement clair.

 
Sélectionnez
"""Framework for getting filetype-specific metadata.

Instantiate appropriate class with filename.  Returned object acts like a
dictionary, with key-value pairs for each piece of metadata.
    import fileinfo
    info = fileinfo.MP3FileInfo("/music/ap/mahadeva.mp3")
    print "\\n".join(["%s=%s" % (k, v) for k, v in info.items()])

Or use listDirectory function to get info on all files in a directory.
    for info in fileinfo.listDirectory("/music/ap/", [".mp3"]):
        ...

Framework can be extended by adding classes for particular file types, e.g.
HTMLFileInfo, MPGFileInfo, DOCFileInfo.  Each class is completely responsible for
parsing its files appropriately; see MP3FileInfo for example.
"""
import os
import sys
from UserDict import UserDict

def stripnulls(data):
    "strip whitespace and nulls"
    return data.replace("\00", "").strip()

class FileInfo(UserDict):
    "store file metadata"
    def __init__(self, filename=None):
        UserDict.__init__(self)
        self["name"] = filename

class MP3FileInfo(FileInfo):
    "store ID3v1.0 MP3 tags"
    tagDataMap = {"title"   : (  3,  33, stripnulls),
                  "artist"  : ( 33,  63, stripnulls),
                  "album"   : ( 63,  93, stripnulls),
                  "year"    : ( 93,  97, stripnulls),
                  "comment" : ( 97, 126, stripnulls),
                  "genre"   : (127, 128, ord)}

    def __parse(self, filename):
        "parse ID3v1.0 tags from MP3 file"
        self.clear()
        try:                               
            fsock = open(filename, "rb", 0)
            try:                           
                fsock.seek(-128, 2)        
                tagdata = fsock.read(128)  
            finally:                       
                fsock.close()              
            if tagdata[:3] == "TAG":
                for tag, (start, end, parseFunc) in self.tagDataMap.items():
                    self[tag] = parseFunc(tagdata[start:end])               
        except IOError:                    
            pass                           

    def __setitem__(self, key, item):
        if key == "name" and item:
            self.__parse(item)
        FileInfo.__setitem__(self, key, item)

def listDirectory(directory, fileExtList):                                        
    "get list of file info objects for files of particular extensions"
    fileList = [os.path.normcase(f)
                for f in os.listdir(directory)]           
    fileList = [os.path.join(directory, f) 
               for f in fileList
                if os.path.splitext(f)[1] in fileExtList] 
    def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):      
        "get file info class from filename extension"                             
        subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]       
        return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
    return [getFileInfoClass(f)(f) for f in fileList]                             

if __name__ == "__main__":
    for info in listDirectory("/music/_singles/", [".mp3"]):
        print "\n".join(["%s=%s" % (k, v) for k, v in info.items()])
        print

Avant de plonger dans le chapitre suivant, assurez vous que vous vous sentez à l'aise pour :

  • Intercepter les exceptions avec try...except
  • Protéger les ressources externes avec try...finally
  • Lire dans des fichiers
  • Assigner des valeurs multiples en une fois dans une boucle for
  • Utiliser le module os pour tous vos besoins de manipulation de fichiers indépendament de la plateforme
  • Instancier des classes de type inconnu dynamiquement en traitant les classes comme des objets

précédentsommairesuivant