Plongez au coeur de Python 11 février 2006 Copyright (c) 2000, 2001, 2002, 2003, 2004 Mark Pilgrim ( mailto:mark@diveintopython.org) Copyright (c) 2001 Xavier Defrang (mailto:xavier@defrang.com) Copyright (c) 2004 Jean-Pierre Gay (mailto:python@kantoche.org) Copyright (c) 2004, 2006 Alexandre Drahon (mailto:python@adrahon.org) Les évolutions de cet ouvrage (et de sa traduction française) sont disponibles sur le site http://diveintopython.org/. Si vous le lisez ailleurs, il est possible que vous ne disposiez pas de la dernière version. Permission vous est donnée de copier, distribuer et/ou modifier ce document selon les termes de la Licence GNU Free Documentation License, Version 1.1 ou ultérieure publiée par la Free Software Foundation, sans Sections Invariables, ni Textes de Première de Couverture, ni Textes de Quatrième de Couverture. Une copie de la licence est incluse en Annexe G, GNU Free Documentation License. Les programmes d'exemple de ce livre sont des logiciels libres, vous pouvez les redistribuer et/ou les modifier selon les termes de la licence Python publiée par la Python Software Foundation. Une copie de la licence est incluse en Annexe H, Python license. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Table des matières • 1. Installation de Python □ 1.1. Quel Python vous faut-il ? □ 1.2. Python sous Windows □ 1.3. Python sous Mac OS X □ 1.4. Python sous Mac OS 9 □ 1.5. Python sous RedHat Linux □ 1.6. Python sous Debian GNU/Linux □ 1.7. Installation de Python à partir du source □ 1.8. L'interface interactive □ 1.9. Résumé • 2. Votre premier programme Python □ 2.1. Plonger □ 2.2. Déclaration de fonctions ☆ 2.2.1. Comparaison des types de données en Python et dans d'autres langages de programmation □ 2.3. Documentation des fonctions □ 2.4. Tout est objet ☆ 2.4.1. Le chemin de recherche d'import ☆ 2.4.2. Qu'est-ce qu'un objet ? □ 2.5. Indentation du code □ 2.6. Test des modules • 3. Types prédéfinis □ 3.1. Présentation des dictionnaires ☆ 3.1.1. Définition des dictionnaires ☆ 3.1.2. Modification des dictionnaires ☆ 3.1.3. Enlever des éléments d'un dictionnaire □ 3.2. Présentation des listes ☆ 3.2.1. Definition d'une liste ☆ 3.2.2. Ajout d'éléments à une liste ☆ 3.2.3. Recherche dans une liste ☆ 3.2.4. Suppression d'éléments d'une liste ☆ 3.2.5. Utilisation des opérateurs de listes □ 3.3. Présentation des tuples □ 3.4. Définitions de variables ☆ 3.4.1. Référencer des variables ☆ 3.4.2. Assignation simultanée de plusieurs valeurs □ 3.5. Formatage de chaînes □ 3.6. Mutation de listes □ 3.7. Jointure de listes et découpage de chaînes ☆ 3.7.1. Note historique sur les méthodes de chaînes □ 3.8. Résumé • 4. Le pouvoir de l’introspection □ 4.1. Plonger □ 4.2. Arguments optionnels et nommés □ 4.3. Utilisation de type, str, dir et autres fonction prédéfinies ☆ 4.3.1. La fonction type ☆ 4.3.2. La fonction str ☆ 4.3.3. Fonctions prédéfinies □ 4.4. Obtenir des références objet avec getattr ☆ 4.4.1. getattr et les modules ☆ 4.4.2. getattr comme sélecteur □ 4.5. Filtrage de listes □ 4.6. Particularités de and et or ☆ 4.6.1. Utilisation de l'astuce and-or □ 4.7. Utiliser des fonctions lambda ☆ 4.7.1. Les fonctions lambda dans le monde réel □ 4.8. Assembler les pièces □ 4.9. Résumé • 5. Les objets et l'orienté objet □ 5.1. Plonger □ 5.2. Importation de modules avec from module import □ 5.3. Définition de classes ☆ 5.3.1. Initialisation et écriture de classes ☆ 5.3.2. Quand utiliser self et __init__ □ 5.4. Instantiation de classes ☆ 5.4.1. Ramasse-miettes □ 5.5. UserDict : une classe enveloppe □ 5.6. Méthodes de classe spéciales ☆ 5.6.1. Lire et écrire des éléments □ 5.7. Méthodes spéciales avancées □ 5.8. Attributs de classe □ 5.9. Fonctions privées □ 5.10. Résumé • 6. Traitement des exceptions et utilisation de fichiers □ 6.1. Traitement des exceptions ☆ 6.1.1. Utilisation d'exceptions pour d'autres cas que la gestion d'erreur □ 6.2. Les objets-fichier ☆ 6.2.1. Lecture d'un fichier ☆ 6.2.2. Fermeture d'un fichier ☆ 6.2.3. Gestion des erreurs d'entrée/sortie ☆ 6.2.4. Ecriture dans un fichier □ 6.3. Itérations avec des boucles for □ 6.4. Utilisation de sys.modules □ 6.5. Travailler avec des répertoires □ 6.6. Assembler les pièces □ 6.7. Résumé • 7. Expressions régulières □ 7.1. Plonger □ 7.2. Exemple : adresses postales □ 7.3. Exemple : chiffres romains ☆ 7.3.1. Rechercher les milliers ☆ 7.3.2. Rechercher les centaines □ 7.4. Utilisation de la syntaxe {n,m} ☆ 7.4.1. Rechercher les dizaines et les unités □ 7.5. Expressions régulières détaillées □ 7.6. Etude de cas : reconnaissance de numéros de téléphone □ 7.7. Résumé • 8. Traitement du HTML □ 8.1. Plonger □ 8.2. Présentation de sgmllib.py □ 8.3. Extraction de données de documents HTML □ 8.4. Présentation de BaseHTMLProcessor.py □ 8.5. locals et globals □ 8.6. Formatage de chaînes à l’aide d’un dictionnaire □ 8.7. Mettre les valeurs d’attributs entre guillemets □ 8.8. Présentation de dialect.py □ 8.9. Assembler les pièces □ 8.10. Résumé • 9. Traitement de données XML □ 9.1. Plonger □ 9.2. Les paquetages □ 9.3. Analyser un document XML □ 9.4. Le standard Unicode □ 9.5. Rechercher des éléments □ 9.6. Accéder aux attributs d'un élément □ 9.7. Transition • 10. Des scripts et des flots de données (streams) □ 10.1. Extraire les sources de données en entrée □ 10.2. Entrée, sortie et erreur standard □ 10.3. Mettre en cache la consultation de noeuds □ 10.4. Trouver les descendants directs d'un noeud □ 10.5. Créer des gestionnaires distincts pour chaque type de noeud □ 10.6. Manipuler les arguments de la ligne de commande □ 10.7. Assembler les pièces □ 10.8. Résumé • 11. Services Web HTTP □ 11.1. Plonger □ 11.2. Obtenir des données par HTTP : la mauvaise méthode □ 11.3. Fonctionnalités de HTTP ☆ 11.3.1. User-Agent ☆ 11.3.2. Les redirections ☆ 11.3.3. Last-Modified/If-Modified-Since ☆ 11.3.4. ETag/If-None-Match ☆ 11.3.5. La compression □ 11.4. débogage de services Web HTTP □ 11.5. Changer la chaîne User-Agent □ 11.6. Prise en charge de Last-Modified et ETag □ 11.7. Prise en charge des redirections. □ 11.8. Prise en charge des données compressées. □ 11.9. Assembler les pièces □ 11.10. Résumé • 12. Services Web SOAP □ 12.1. Plonger □ 12.2. Installation des bibliothèques SOAP ☆ 12.2.1. Installation PyXML ☆ 12.2.2. Installation de fpconst ☆ 12.2.3. Installation de SOAPpy □ 12.3. Premiers pas avec SOAP □ 12.4. Débogage de services Web SOAP □ 12.5. Présentation de WSDL □ 12.6. Introspection de services Web SOAP avec WSDL □ 12.7. Recherche Google □ 12.8. Recherche d'erreurs dans les services Web SOAP □ 12.9. Résumé • 13. Tests unitaires □ 13.1. Introduction au chiffres romains □ 13.2. Présentation de romantest.py □ 13.3. Présentation de romantest.py □ 13.4. Tester la réussite □ 13.5. Tester l’échec □ 13.6. Tester la cohérence • 14. Ecriture des tests en premier □ 14.1. roman.py, étape 1 □ 14.2. roman.py, étape 2 □ 14.3. roman.py, étape 3 □ 14.4. roman.py, étape 4 □ 14.5. roman.py, étape 5 • 15. Refactorisation □ 15.1. Gestion des bogues □ 15.2. Gestion des changements de spécification □ 15.3. Refactorisation □ 15.4. Postscriptum □ 15.5. Résumé • 16. Programmation fonctionnelle □ 16.1. Plonger □ 16.2. Trouver le chemin □ 16.3. Le filtrage de liste revisité □ 16.4. La mutation de liste revisitée □ 16.5. Programmation centrée sur les données □ 16.6. Importation dynamique de modules □ 16.7. Assembler les pièces □ 16.8. Résumé • 17. Fonctions dynamiques □ 17.1. Plonger □ 17.2. plural.py, étape 1 □ 17.3. plural.py, étape 2 □ 17.4. plural.py, étape 3 □ 17.5. plural.py, étape 4 □ 17.6. plural.py, étape 5 □ 17.7. plural.py, étape 6 □ 17.8. Résumé • 18. Ajustements des performances □ 18.1. Plonger □ 18.2. Utilisation du module timeit □ 18.3. Optimisation d'expressions régulières □ 18.4. Optimisation de la lecture d'un dictionnaire □ 18.5. Optimisation des opérations sur les listes □ 18.6. Optimisation des manipulations de chaînes □ 18.7. Résumé • A. Pour en savoir plus • B. Survol en cinq minutes • C. Trucs et astuces • D. Liste des exemples • E. Historique des révisions • F. A propos de ce livre • G. GNU Free Documentation License □ G.0. Preamble □ G.1. Applicability and definitions □ G.2. Verbatim copying □ G.3. Copying in quantity □ G.4. Modifications □ G.5. Combining documents □ G.6. Collections of documents □ G.7. Aggregation with independent works □ G.8. Translation □ G.9. Termination □ G.10. Future revisions of this license □ G.11. How to use this License for your documents • H. Python license □ H.A. History of the software □ H.B. Terms and conditions for accessing or otherwise using Python ☆ H.B.1. PSF license agreement ☆ H.B.2. BeOpen Python open source license agreement version 1 ☆ H.B.3. CNRI open source GPL-compatible license agreement ☆ H.B.4. CWI permissions statement and disclaimer Chapitre 1. Installation de Python Bienvenue dans le monde de Python. Préparez-vous à plonger. Dans ce chapitre, nous allons installer une version de Python appropriée à votre situation. 1.1. Quel Python vous faut-il ? La première chose à faire avec Python est de l'installer. Mais est-ce nécéssaire ? Si vous utilisez un compte sur un serveur hébergé, votre FAI a peut-être déjà installé Python. La plupart des distributions Linux les plus courantes installent Python par défaut. Mac OS X 10.2 et les versions suivantes comprennent une version en ligne de commande de Python, mais vous souhaiterez sans doute installer une version ayant une interface graphique plus typique du Mac. Windows n'inclut aucune version de Python, mais ne désespérez pas ! Il y a de nombreuses manières d'installer Python sous Windows. Comme vous pouvez le constater, Python supporte un grand nombre de systèmes d'exploitation. La liste complète comprend Windows, Mac OS, Mac OS X et tous les systèmes libres compatibles UNIX comme Linux. Il y a aussi des versions pour Sun Solaris, AS/400, Amiga, OS/2, BeOS et une pléthore d'autres plate-formes dont vous n'avez sans doute jamais entendu parler. De plus, les programmes Python écrits sur une plate-forme peuvent, en prenant en compte certain détails, être exécutés sur toutes les plate-formes supportées. Par exemple, je développe régulièrement des programmes Python sous Windows pour les déployer ensuite sous Linux. Mais revenons à la question de départ, <> Celle qui fonctionne sur la plate-forme que vous utilisez. 1.2. Python sous Windows Sous Windows, il y a plusieurs possibilités pour installer Python. ActiveState propose un programme d'installation pour Windows appelé ActivePython, qui comprend une version complète de Python, un IDE doté d'un éditeur de code prenant en compte Python et quelques extensions spécifiques à Windows pour Python qui permettent d'accèder aux services et aux APIs propres à Windows, ainsi qu'à la Base de registre. ActivePython est librement téléchargeable, bien qu'il ne soit pas open source. C'est l'IDE que j'ai utilisé pour apprendre Python et je vous recommande de l'essayer, à moins que vous n'ayez une raison spécifique de ne pas le faire. Une de ces raisons pourrait être qu'ActiveState a généralement plusieurs mois de retard quand une nouvelle version de Python est publiée. Si vous avez absolument besoin de la dernière version de Python et qu'ActivePython a une version de retard, vous pourrez choisir la deuxième option pour installer Python sous Windows. La deuxième option est le programme d'installation <> de Python, distribué par les gens qui développent Python. Il est librement téléchargeable et open source et il installe toujours la version la plus récente de Python. Procédure 1.1. Option 1 : installer ActivePython Voici la procédure pour installer ActivePython : 1. Téléchargez ActivePython depuis http://www.activestate.com/ASPN/Downloads/ ActivePython/. 2. Si vous utilisez Windows 95, Windows 98 ou Windows ME, vous aurez également besoin de télécharger et d'installer Windows Installer 2.0 (http:// download.microsoft.com/download/WindowsInstaller/Install/2.0/W9XMe/EN-US/ InstMsiA.exe) avant d'installer ActivePython. 3. Double-cliquez sur le programme d'installation, ActivePython-2.2.2-224-win32-ix86.msi. 4. Suivez les étapes du programme d'installation. 5. Si vous manquez d'espace disque, vous pouvez sélectionner l'installation personnalisée et déselectionner la documentation, mais je ne vous le recommande pas, à moins que vous ayez vraiment besoin des 14 Mo. 6. Lorsque l'installation est terminée, fermez le programme d'installation et lancez Démarrer->Programmes->ActiveState ActivePython 2.2->PythonWin IDE. Vous verrez quelque chose comme l'écran suivant : PythonWin 2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)] on win32. Portions Copyright 1994-2001 Mark Hammond (mhammond@skippinet.com.au) - see 'Help/About PythonWin' for further copyright information. >>> Procédure 1.2. Option 2 : installer Python depuis Python.org (http:// www.python.org/) 1. Téléchargez la dernière version du programme d'installation Python pour Windows depuis http://www.python.org/ftp/python/2.3.3/ en sélectionnant le numéro de version le plus haut et en téléchargeant le programme .exe. 2. Double-cliquez sur le programme d'installation, Python-2.xxx.yyy.exe. Le nom dépend de la version de Python disponible au moment du téléchargement. 3. Suivez les étapes du programme d'installation. 4. Si vous manquez d'espace disque, vous pouvez déselectionner le fichier d'aide HTMLHelp, les scripts utilitaires (Tools/) et/ou la suite de tests (Lib/test/). 5. Si vous n'avez pas les droits d'administrateur de la machine, vous pouvez sélectionner Advanced Options, puis choisir Non-Admin Install. Cette option modifie uniquement l'emplacement des entrées de la Base de registre et des raccourcis du menu Démarrer. 6. Après l'achèvement de l'installation, fermez le programme d'installation et lancez Démarrer->Programmes->Python 2.3->IDLE (Python GUI). Vous verrez quelque chose comme l'écran suivant : Python 2.3.2 (#49, Oct 2 2003, 20:02:00) [MSC v.1200 32 bit (Intel)] on win32 Type "copyright", "credits" or "license()" for more information. **************************************************************** Personal firewall software may warn about the connection IDLE makes to its subprocess using this computer's internal loopback interface. This connection is not visible on any external interface and no data is sent to or received from the Internet. **************************************************************** IDLE 1.0 >>> 1.3. Python sous Mac OS X Sous Mac OS X, vous avez deux possibilités, garder la version préinstallée ou installer une nouvelle version. Vous préfèrerez sans doute cette dernière solution. Mac OS X 10.2 et les version ultérieures contiennent une version en ligne de commande de Python. Si vous êtes à l'aise avec la ligne de commande, vous pouvez utiliser cette version pour le premier tiers du livre. Par contre, la version préinstallée ne contient pas d'analyseur pour le XML, vous devrez donc installer la version complète lorsque vous arriverez au chapitre traitant du XML. Installer la dernière version présente également l'avantage d'une interface graphique interactive. Procédure 1.3. Exécuter la version préinstallée de Python sous Mac OS X Pour utiliser la version préinstallée de Python, suivez ces étapes : 1. Ouvrez le dossier /Applications. 2. Ouvrez le dossier Utilitaires. 3. Double-cliquez sur Terminal pour ouvrir une fenêtre de terminal et accéder à la ligne de commande. 4. Tapez python sur la ligne de commande. Essayez : Welcome to Darwin! [localhost:~] you% python Python 2.2 (#1, 07/14/02, 23:25:09) [GCC Apple cpp-precomp 6.14] on darwin Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] [localhost:~] you% Procédure 1.4. Installation de la dernière version de Python sous Mac OS X Suivez ces étapes pour télécharger et installer la dernière version de Python: 1. Téléchargez l'image disque MacPython-OSX depuis http://www.cwi.nl/~jack/ macpython.html. Si votre navigateur n'a pas monté l'image, double-cliquez sur MacPython-OSX-2.3-1.dmg pour la monter sur le bureau. 2. Double-cliquer sur le programme d'installation, MacPython-OSX.pkg. 3. Le programme d'installation vous demandera votre nom et mot de passe d'administrateur. 4. Suivez les étapes du programme d'installation. 5. Lorsque l'installation est terminée, fermez le programme d'installation et ouvrez le dossier /Applications. 6. Ouvrez le dossier MacPython-2.3. 7. Double-cliquez sur PythonIDE pour lancer Python. L'IDE MacPython devrait afficher un écran d'accueil, puis vous amener à l'interface interactive. Si l'interface n'apparaît pas, sélectionnez Window-> Python Interactive (Cmd-0). La fenêtre qui s'ouvre ressemblera à l'écran suivant : Python 2.3 (#2, Jul 30 2003, 11:45:28) [GCC 3.1 20020420 (prerelease)] Type "copyright", "credits" or "license" for more information. MacPython IDE 1.0.1 >>> Notez qu'une fois que vous installez la dernière version, la version préinstallée est toujours présente. Si vous exécutez des scripts depuis la ligne de commande, vous devez savoir quelle version de Python vous utilisez. Exemple 1.1. Deux versions de Python [localhost:~] you% python Python 2.2 (#1, 07/14/02, 23:25:09) [GCC Apple cpp-precomp 6.14] on darwin Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] [localhost:~] you% /usr/local/bin/python Python 2.3 (#2, Jul 30 2003, 11:45:28) [GCC 3.1 20020420 (prerelease)] on darwin Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] [localhost:~] you% 1.4. Python sous Mac OS 9 Mac OS 9 n'est fournit avec aucune version de Python, mais l'installation en est très simple. Suivez ces étapes pour installer Python sous Mac OS 9 : 1. Téléchargez le fichier MacPython23full.bin depuis http://www.cwi.nl/~jack/ macpython.html. 2. Si votre navigateur n'a pas décompressé le fichier automatiquement, double-cliquez MacPython23full.bin pour le décompresser avec Stuffit Expander. 3. Double-cliquez sur le programme d'installation, MacPython23full. 4. Suivez les étapes du programme d'installation. 5. Lorsque l'installation est terminée, fermez le programme d'installation et ouvrez le dossier /Applications. 6. Ouvrez le dossier MacPython-OS9 2.3. 7. Double-cliquez Python IDE pour lancer Python. L'IDE MacPython devrait afficher un écran d'accueil, puis vous amener à l'interface interactive. Si l'interface n'apparaît pas, sélectionnez Window-> Python Interactive (Cmd-0). La fenêtre qui s'ouvre ressemblera à l'écran suivant : Python 2.3 (#2, Jul 30 2003, 11:45:28) [GCC 3.1 20020420 (prerelease)] Type "copyright", "credits" or "license" for more information. MacPython IDE 1.0.1 >>> 1.5. Python sous RedHat Linux L'installation sous un système d'exploitation compatible UNIX tel que Linux est simple si vous choisissez l'installation d'un paquetage binaire. Des paquetage binaires précompilés sont disponibles pour les distributions Linux les plus répandues. Vous pouvez également compiler à partir des sources. Téléchargez le dernier RPM Python en allant sur http://www.python.org/ftp/ python/2.3.3/ et en sélectionnant le numéro de version le plus haut, puis en sélectionnant le sous-répertoire rpms/ de cette version. Téléchargez ensuite le RPM ayant le plus haut numéro de version. Vous pouvez l'installer avec la commande rpm, comme ci-dessous : Exemple 1.2. Installation sous RedHat Linux 9 localhost:~$ su - Password: [enter your root password] [root@localhost root]# wget http://python.org/ftp/python/2.3/rpms/redhat-9/python2.3-2.3-5pydotorg.i386.rpm Resolving python.org... done. Connecting to python.org[194.109.137.226]:80... connected. HTTP request sent, awaiting response... 200 OK Length: 7,495,111 [application/octet-stream] ... [root@localhost root]# rpm -Uvh python2.3-2.3-5pydotorg.i386.rpm Preparing... ########################################### [100%] 1:python2.3 ########################################### [100%] [root@localhost root]# python (1) Python 2.2.2 (#1, Feb 24 2003, 19:13:11) [GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-4)] on linux2 Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to exit] [root@localhost root]# python2.3 (2) Python 2.3 (#1, Sep 12 2003, 10:53:56) [GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2 Type "help", "copyright", "credits", or "license" for more information. >>> [press Ctrl+D to exit] [root@localhost root]# which python2.3 (3) /usr/bin/python2.3 (1) Attention ! Taper simplement python lance l'ancienne version de Python -- celle qui était installée par défaut. Ce n'est pas celle que vous voulez. (2) Au moment où j'écris, la version la plus récente s'appelle python2.3. Vous aurez sans doute à changer le chemin à la première ligne du script pour pointer vers une version plus récente. (3) C'est le chemin complet de la version la plus récente de Python que vous venez d'installer. Utilisez-le sur la ligne #! (la première ligne de chaque script) pour vous assurez que les scripts utilisent la dernière version de Python et faites bien attention de taper python2.3 pour accéder à l'interface interactive. 1.6. Python sous Debian GNU/Linux Si vous avez la chance d'utiliser Debian GNU/Linux, vous pouvez installer Python à l'aide de la commande apt. Exemple 1.3. Installation sous Debian GNU/Linux localhost:~$ su - Password: [enter your root password] localhost:~# apt-get install python Reading Package Lists... Done Building Dependency Tree... Done The following extra packages will be installed: python2.3 Suggested packages: python-tk python2.3-doc The following NEW packages will be installed: python python2.3 0 upgraded, 2 newly installed, 0 to remove and 3 not upgraded. Need to get 0B/2880kB of archives. After unpacking 9351kB of additional disk space will be used. Do you want to continue? [Y/n] Y Selecting previously deselected package python2.3. (Reading database ... 22848 files and directories currently installed.) Unpacking python2.3 (from .../python2.3_2.3.1-1_i386.deb) ... Selecting previously deselected package python. Unpacking python (from .../python_2.3.1-1_all.deb) ... Setting up python (2.3.1-1) ... Setting up python2.3 (2.3.1-1) ... Compiling python modules in /usr/lib/python2.3 ... Compiling optimized python modules in /usr/lib/python2.3 ... localhost:~# exit logout localhost:~$ python Python 2.3.1 (#2, Sep 24 2003, 11:39:14) [GCC 3.3.2 20030908 (Debian prerelease)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> [press Ctrl+D to exit] 1.7. Installation de Python à partir du source Si vous préférez installer à partir du source, vous pouvez télécharger le code source de Python à partir de http://www.python.org/ftp/python/2.3.3/. Sélectionnez le numéro de version le plus haut de la liste, téléchargez le fichier .tgz et faites la séquence habituelle configure, make, make install. Exemple 1.4. Installation à partir du source localhost:~$ su - Password: [enter your root password] localhost:~# wget http://www.python.org/ftp/python/2.3/Python-2.3.tgz Resolving www.python.org... done. Connecting to www.python.org[194.109.137.226]:80... connected. HTTP request sent, awaiting response... 200 OK Length: 8,436,880 [application/x-tar] ... localhost:~# tar xfz Python-2.3.tgz localhost:~# cd Python-2.3 localhost:~/Python-2.3# ./configure checking MACHDEP... linux2 checking EXTRAPLATDIR... checking for --without-gcc... no ... localhost:~/Python-2.3# make gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I. -I./Include -DPy_BUILD_CORE -o Modules/python.o Modules/python.c gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I. -I./Include -DPy_BUILD_CORE -o Parser/acceler.o Parser/acceler.c gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I. -I./Include -DPy_BUILD_CORE -o Parser/grammar1.o Parser/grammar1.c ... localhost:~/Python-2.3# make install /usr/bin/install -c python /usr/local/bin/python2.3 ... localhost:~/Python-2.3# exit logout localhost:~$ which python /usr/local/bin/python localhost:~$ python Python 2.3.1 (#2, Sep 24 2003, 11:39:14) [GCC 3.3.2 20030908 (Debian prerelease)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> [press Ctrl+D to get back to the command prompt] localhost:~$ 1.8. L'interface interactive Maintenant que Python est installé, nous allons voir en quoi consiste cette interface interactive que vous avez lancé. Il faut comprendre une chose : Python mène une double vie. C'est un interpréteur de scripts que vous pouvez lancer depuis la ligne de commande, ou en double-cliquant sur un script. Mais c'est aussi une interface interactive qui peut évaluer n'importe quelle instruction ou expression. C'est très utile pour le débogage, pour écrire rapidement du code et pour les tests. Je connais même des gens qui utilisent l'interface interactive de Python comme calculatrice ! Lancez l'interface interactive de Python de la manière qui convient à plate-forme et commençons la plongée par les étapes suivantes : Exemple 1.5. Premiers pas dans l'interface interactive >>> 1 + 1 (1) 2 >>> print 'hello world' (2) hello world >>> x = 1 (3) >>> y = 2 >>> x + y 3 (1) L'interface interactive de Python peut évaluer une expression Python quelconque, y compris une expression arithmétique de base. (2) L'interface interactive peut exécuter des instructions Python, y compris l'instruction print. (3) Vous pouvez aussi assigner des valeurs à des variables et les valeurs seront conservées aussi longtemps que l'interface est ouverte (mais pas plus longtemps que cela). 1.9. Résumé Vous devez maintenant avoir une version de Python installée et fonctionnelle. En fonction de votre plate-forme, vous pouvez avoir plus d'une version de Python installée. Si c'est le cas, vous devez connaître vos chemins d'accès. Si entrer simplement python sur la ligne de commande ne lance pas la version de Python que vous souhaitez utiliser, vous aurez peut être à entrer le chemin complet de votre version préférée. Félicitations et bienvenue dans le monde de Python. Chapitre 2. Votre premier programme Python La plupart des autres livres expliquent pas à pas les concepts de programmation pour vous amener à la fin à l'écriture d'un programme complet et fonctionnel. Et bien nous allons sauter toutes ces étapes. 2.1. Plonger Voici un programme Python complet et fonctionnel. Il ne signifie probablement rien pour vous. Ne vous en faites pas, nous allons le disséquer ligne par ligne. Mais lisez-le et voyez ce que vous pouvez déjà en retirer. Exemple 2.1. odbchelper.py Si vous ne l’avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples (http://diveintopython.org/download/ diveintopython-examples-5.4.zip) du livre. def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string.""" return ";".join(["%s=%s" % (k, v) for k, v in params.items()]) if __name__ == "__main__": myParams = {"server":"mpilgrim", \ "database":"master", \ "uid":"sa", \ "pwd":"secret" \ } print buildConnectionString(myParams) Lancez maintenant ce programme et observez ce qui se passe. ASTUCE: Exécuter un programme sous Windows Dans l'IDE ActivePython sous Windows, vous pouvez exécuter le programme Python que vous êtes en train d'éditer par File->Run... (Ctrl-R). La sortie est affichée dans la fenêtre interactive. ASTUCE: Exécuter un programme sous Mac OS Dans l'IDE Python sous Mac OS, vous pouvez exécuter un module avec Python-> Run window... (Cmd-R) mais il y a une option importante que vous devez activer préalablement. Ouvrez le module dans l'IDE, ouvrez le menu des options des modules en cliquant le triangle noir dans le coin supérieur droit de la fenêtre et assurez-vous que <> est coché. Ce réglage est sauvegardé avec le module, vous n'avez donc à faire cette manipulation qu'une fois par module. ASTUCE: Exécuter un programe sous UNIX Sur les systèmes compatibles UNIX (y compris Mac OS X), vous pouvez exécuter un programme Python depuis la ligne de commande : python odbchelper.py La sortie de odbchelper.py ressemblera à l'écran suivant : server=mpilgrim;uid=sa;database=master;pwd=secret 2.2. Déclaration de fonctions Python dispose de fonctions comme la plupart des autre langages, mais il n'a pas de fichiers d'en-tête séparés comme C++ ou de sections interface/ implementation comme Pascal. Lorsque vous avez besoin d'une fonction, vous n'avez qu'à la déclarer et l'écrire. def buildConnectionString(params): Il y a plusieurs remarques à faire. Premièrement, le mot clé def débute une déclaration de fonction, suivi du nom de la fonction, puis des arguments entre parenthèses. Les arguments multiples (non montré ici) sont séparés par des virgules. Deuxièmement, la fonction ne défini pas le type de données qu'elle retourne. Les fonctions Python ne définissent pas le type de leur valeur de retour, elle ne spécifient même pas si elle retournent une valeur ou pas. En fait chaque fonction Python retourne une valeur, si la fonction exécute une instruction return, elle va en retourner la valeur, sinon elle retournera None, la valeur nulle en Python. NOTE: Python vs. Visual Basic : les valeurs de retour En Visual Basic, les fonctions (qui retournent une valeur) débutent avec function et les sous-routines (qui ne retourne aucune valeur) débutent avec sub. Il n'y a pas de sous-routines en Python. Tout est fonction, toute fonction retourne un valeur (même si c'est None) et toute fonction débute avec def. Troisièmement, les arguments, params, ne spécifient pas de types de données. En Python, les variables ne sont jamais explicitement typées. Python détermine le type d'une variable et en garde la trace en interne. NOTE: Python vs. Java : les valeurs de retour En Java, C++ et autres langage à typage statique, vous devez spécifier les types de données de la valeur de retour d'une fonction ainsi que de chaque paramètre. En Python, vous ne spécifiez jamais de manière explicite le type de quoi que ce soit. En se basant sur la valeur que vous lui assignez, Python gère les types de données en interne. 2.2.1. Comparaison des types de données en Python et dans d'autres langages de programmation Un lecteur érudit propose l'explication suivante pour comparer Python et les autres langages de programmation : langage à typage statique Un langage dans lequel les types sont fixés à la compilation. La plupart des langages à typage statique obtiennent cela en exigeant la déclaration de toutes les variables et de leur type avant leur utilisation. Java et C sont des langages à typage statique. langage à typage dynamique Un langage dans lequel les types sont découverts à l'exécution, l'inverse du typage statique. VBScript et Python sont des langages à typage dynamique, ils déterminent le type d'une variable la première fois que vous lui assignez une valeur. langage fortement typé Un langage dans lequel les types sont toujours appliqués. Java et Python sont fortement typés. Un entier ne peut être traité comme une chaîne sans conversion explicite langage faiblement typé Un langage dans lequel les types peuvent être ignorés, l'inverse de fortement typé. VBScript est faiblement typé. En VBScript, vous pouvez concaténer la chaîne '12' et l'entier 3 pour obtenir la chaîne '123' et traiter le résultat comme l'entier 123, le tout sans faire de conversion explicite. Python est donc à la fois à typage dynamique (il n'utilise pas de déclaration de type explicite) et fortement typé (une fois qu'une variable a un type, cela a une importance). 2.3. Documentation des fonctions Vous pouvez documenter une fonction Python en lui donnant une chaîne de documentation (doc string). Exemple 2.2. Définition d'une doc string pour la fonction buildConnectionString def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string.""" Les tripes guillemets indiquent une chaîne multi-lignes. Tout ce qu'il y a entre l'ouverture et la fermeture des guillemets fait partie de la chaîne, y compris les retours chariot et les autres guillemets. On peut les utiliser partout, mais vous les verrez le plus souvent utilisées pour définir une doc string. NOTE: Python vs. Perl : guillemets Les triples guillemets sont aussi un moyen simple de définir une chaîne contenant à la fois des guillemets simples et doubles, comme qq/.../ en Perl. Tout ce qui se trouve entre les triples guillemets fait partie de la doc string de la fonction, qui décrit ce que fait la fonction. Une doc string, si elle existe, doit être la première chose déclarée dans une fonction (la première chose après les deux points). Techniquement parlant, vous n'êtes pas obligés de donner une doc string à votre fonction, mais vous devriez toujours le faire. Je sais que vous avez entendu cela à tous les cours de programmation auxquels vous avez assisté mais Python vous donne une motivation supplémentaire : la doc string est disponible à l'exécution en tant qu'attribut de fonction. NOTE: Pourquoi les doc string sont une Bonne Chose Beaucoup d'IDE Python utilisent les doc string pour fournir une documentation contextuelle, ainsi lorsque vous tapez le nom d'une fonction, sa doc string apparaît dans une bulle d'aide. Cela peut être incroyablement utile, mais cette utilité est liée à la qualité de votre doc string. Pour en savoir plus sur la documentation des fonctions • La PEP 257 (http://www.python.org/peps/pep-0257.html) définit les conventions pour les doc string. • Le Python Style Guide (http://www.python.org/doc/essays/styleguide.html) explique la manière d'écrire de bonnes doc string. • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite des conventions d'espacement dans les doc string (http://www.python.org/doc /current/tut/node6.html#SECTION006750000000000000000). 2.4. Tout est objet Au cas ou vous ne l'auriez pas noté, je viens de dire que les fonctions Python ont des attributs et que ces attributs étaient disponibles au moment de l'exécution. Une fonction, comme tout le reste en Python, est un objet. Ouvrez votre IDE Python favorite et suivez ces étapes : Exemple 2.3. Accéder à la doc string de la fonction buildConnectionString >>> import odbchelper (1) >>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> print odbchelper.buildConnectionString(params) (2) server=mpilgrim;uid=sa;database=master;pwd=secret >>> print odbchelper.buildConnectionString.__doc__ (3) Build a connection string from a dictionary Returns string. (1) La première ligne importe le programme odbchelper comme module -- un morceau de code qui peut être utilisé interactivement ou depuis un programme Python (vous verrez des exemples de programmes Python multimodules au Chapitre 4). Une fois que vous importez un module, vous pouvez référencer chacune de ses fonctions, classes ou attributs publics. Les modules peuvent faire cela pour accéder aux fonctionnalités offertes par d'autres modules et vous pouvez le faire dans l'IDE également. C'est un concept important et nous allons en discuter plus amplement plus tard. (2) Quand vous souhaitez utiliser des fonctions définies dans un module importé, vous devez inclure le nom du module. Vous ne pouvez donc pas dire buildConnectionString, ce doit être odbchelper.buildConnectionString. Si vous avez utilisé des classes en Java, cela devrait vous sembler vaguement familier. (3) Plutôt que d'appeler la fonction comme vous l'auriez attendu, nous demandons un des attributs de la fonction, __doc__. NOTE: Python vs. Perl : import import en Python est similaire à require en Perl. Une fois que vous importez un module Python, vous accédez à ses fonctions avec module.function. Une fois que vous incluez un module Perl, vous accédez à ses fonctions avec module::function. 2.4.1. Le chemin de recherche d'import Avant d'aller plus loin, je veux mentionner rapidement le chemin de recherche de bibliothèques. Python cherche dans plusieurs endroits lorsque vous essayez d'importer un module. Plus précisément, il regarde dans tous les répertoires définis dans sys.path. C'est une simple liste et vous pouvez facilement la voir ou la modifier à l'aide des méthodes standard de listes (nous en apprendrons plus sur les listes plus loin dans ce chapitre). Exemple 2.4. Chemin de recherche d'import >>> import sys (1) >>> sys.path (2) ['', '/usr/local/lib/python2.2', '/usr/local/lib/python2.2/plat-linux2', '/usr/local/lib/python2.2/lib-dynload', '/usr/local/lib/python2.2/site-packages', '/usr/local/lib/python2.2/site-packages/PIL', '/usr/local/lib/python2.2/site-packages/piddle'] >>> sys (3) >>> sys.path.append('/my/new/path') (4) (1) Importer le module sys rend toutes ses fonctions et attributs disponibles. (2) sys.path est une liste de répertoires qui constitue le chemin de recherche actuel (le votre sera différent en fonction de votre système d'exploitation, la version de Python que vous utilisez et l'endroit où vous l'avez installé). Python recherchera dans ces repertoires (dans l'ordre donné) un fichier .py portant le nom de module que vous tentez d'importer. (3) En fait j'ai menti, la réalité est plus compliquée que ça car tous les modules ne sont pas dans des fichiers .py. Certains, comme le module sys, sont des modules intégrés, il sont inclus dans Python lui-même. Les modules intégrés se comportent comme des modules ordinaires, mais leur code source Python n'est pas disponible car il ne sont pas écrits en Python (le module sys est écrit en C). (4) Vous pouvez ajouter un nouveau répertoire au chemin de recherche de Python en le joignant à sys.path et Python cherchera dans ce répertoire également lorsque vous essayez d'importer un module. Cela dure tant que Python tourne (nous reparlerons de append (joindre) et des autres méthodes de listes au Chapitre 3). 2.4.2. Qu'est-ce qu'un objet ? En Python, tout est objet et presque tout dispose d'attributs et de méthodes. Toutes les fonctions ont un attribut prédéfini __doc__ qui retourne la doc string définie dans le code source de la fonction. Le module sys est un objet qui a (entre autres choses) un attribut appelé path. Et ainsi de suite. Reste la question : qu'est-ce qu'un objet ? Chaque langage de programmation définit le terme <> à sa manière. Pour certain, cela signifie que tout objet doit avoir des attributs et des méthodes, pour d'autres, cela signifie que tout les objets doivent être dérivables. En Python, la définition est plus flexible. Certains objets n'ont ni attributs ni méthodes (nous verrons cela au Chapitre 3) et tous les objets ne sont pas dérivables (voir le Chapitre 5). Mais tout est objet dans le sens où tout peut être assigné à une variable ou passé comme argument à une fonction (voir au Chapitre 4). Ceci est important et il ne fait aucun mal de le souligner une derniere fois: en Python tout est objet. Les chaînes sont des objets. Les listes sont des objets. Les fonctions sont des objets. Même les modules sont des objets. Pour en savoir plus sur les objets • La Python Reference Manual (http://www.python.org/doc/current/ref/) explique précisémment ce qu'implique de dire que tout est objet en Python ( http://www.python.org/doc/current/ref/objects.html), puisque certains pédants aiment discuter longuement de ce genre de choses. • eff-bot (http://www.effbot.org/guides/) propose un résumé des objets Python (http://www.effbot.org/guides/python-objects.htm). 2.5. Indentation du code Les fonctions Python n'ont pas de begin ou end explicites, ni d'accolades qui pourraient marquer là ou commence et ou se termine le code de la fonction. Le seul délimiteur est les deux points (<<:>>) et l'indentation du code lui-même. Exemple 2.5. Indentation de la fonction buildConnectionString def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string.""" return ";".join(["%s=%s" % (k, v) for k, v in params.items()]) Les blocs de code (fonctions, instructions if, boucles for ou while etc.) sont définis par leur indentation. L'indentation démarre le bloc et la désindendation le termine. Il n'y a pas d'accolades, de crochets ou de mots clés spécifiques. Cela signifie que les espaces blancs sont significatifs et qu'ils doivent être cohérents. Dans cet exemple, le code de la fonction (y compris sa doc string) sont indentés de 4 espaces. Cela ne doit pas être forcément 4 espaces, mais il faut que ce soit cohérent. La première ligne non indentée est en dehors de la fonction. L'Exemple 2.6, «Instructions if» montre un exemple d'indentation du code avec des instructions if. Exemple 2.6. Instructions if def fib(n): (1) print 'n =', n (2) if n > 1: (3) return n * fib(n - 1) else: (4) print 'end of the line' return 1 (1) Voici une fonction nommée fib qui prend un argument, n. Tout le code de cette fonction est indenté. (2) Afficher une sortie à l'écran est très facile en Python, il suffit d'utiliser print. Les instructions print peuvent prendre n'importe quel type de données, y compris les chaînes, les entiers et d'autres types prédéfinis comme les dictionnaires et les listes, que vous découvrirez dans le prochain chapitre. Vous pouvez même mélanger les types pour imprimer plusieurs éléments sur la même ligne en utilisant une liste de valeurs séparées par des virgules. Chaque valeur est affichée sur la même ligne, séparée par des espaces (les virgules ne sont pas imprimées). Donc, lorsque fib est appelé avec 5, cette ligne affichera "n = 5". (3) Les instructions if sont un type de bloc de code. Si l'expression if est évaluée à vrai, le bloc de code indenté est exécuté, sinon on saute au bloc else. (4) Bien sûr, les blocs if et else peuvent contenir des lignes multiples, tant qu'elles sont toutes indentées au même niveau. Ce bloc else contient deux lignes de code. Il n'y a pas d'autre syntaxe pour les blocs de codes multilignes. Indentez et c'est tout. Après quelques protestations initiales et des analogies méprisantes à Fortran, vous vous en accomoderez et commencerez à en voir les bénéfices. Un des bénéfices majeurs est que tous les programmes Python ont la même apparence puisque l'indentation est une caractéristique du langage et non une question de style. Cela rend plus simple à comprendre le code Python des autres. NOTE: Python vs. Java : séparation des instructions Python utilise le retour chariot pour séparer les instructions, deux points et l'indentation pour séparer les blocs de code. C++ et Java utilisent des points-virgules pour séparer les instructions et des accolades pour séparer les blocs de code. Pour en savoir plus sur l'indentation du code • La Python Reference Manual (http://www.python.org/doc/current/ref/) discute des aspects multiplate-formes de l'indentation et présente diverses erreurs d'indentation (http://www.python.org/doc/current/ref/indentation.html). • Le Python Style Guide (http://www.python.org/doc/essays/styleguide.html) discute du bon usage de l'indentation. 2.6. Test des modules Les modules Python sont des objets et ils ont de nombreux attributs utiles. C'est un aspect que vous pouvez utiliser pour tester facilement vos modules au cours de leur écriture. Voici un exemple qui utilise l'astuce if __name__. if __name__ == "__main__": Quelques remarques avant de passer aux choses sérieuses. Premièrement, les parenthèses ne sont pas obligatoires autour de l'expression if. Ensuite, l'instruction if se termine par deux points et est suivie de code indenté. NOTE: Python vs. C: comparaison et assignation A l'instar de C, Python utilise == pour la comparaison et = pour l'assignement. Mais au contraire de C, Python ne permet pas les assignations dans le corps d'une instruction afin d'éviter qu'une valeur soit accidentellement assignée alors que vous pensiez effectuer une simple comparaison. En quoi cette instruction if est-elle une astuce ? Les modules sont des objets et tous les modules disposent de l'attribut prédéfini __name__. Le __name__ d'un module dépend de la façon dont vous l'utilisez. Si vous importez le module, son __name__ est le nom de fichier du module sans le chemin d'accès ni le suffixe. Mais vous pouvez aussi lancer le module directement en tant que programme, dans ce cas __name__ va prendre par défaut une valeur spéciale, __main__. >>> import odbchelper >>> odbchelper.__name__ 'odbchelper' Sachant cela, vous pouvez concevoir une suite de tests pour votre module au sein même de ce dernier en la plaçant dans ce if. Quand vous lancez le module directement, __name__ est __main__ et la séquence de tests s'exécute. Quand vous importez le module, __name__ est autre chose et les tests sont ignorés. Cela facilite le développement et le déboguage de nouveaux modules avant leur intégration dans un programme plus grand. ASTUCE: if __name__ sous Mac OS Avec MacPython, il y a une étape supplémentaire pour que l'astuce if __name__ fonctionne. Ouvrez le menu des options des modules en cliquant le triangle noir dans le coin supérieur droit de la fenêtre et assurez-vous que Run as __main__ est coché. Pour en savoir plus sur l'importation des modules • La Python Reference Manual (http://www.python.org/doc/current/ref/) explique les détails techniques de l'importation de modules (http:// www.python.org/doc/current/ref/import.html). Chapitre 3. Types prédéfinis Avant de revenir à votre premier programme Python, une petite digression est de rigueur car vous devez absolument connaître les dictionnaires, les tuples et les listes (tout ça !). Si vous être un programmeur Perl, vous pouvez probablement passer rapidemment sur les points concernant les dictionnaires et les listes mais vous devrez quand même faire attention aux tuples. 3.1. Présentation des dictionnaires Un des types de données fondamentaux de Python est le dictionnaire, qui défini une relation 1 à 1 entre des clés et des valeurs. NOTE: Python vs. Perl: les dictionnaires En Python, un dictionnaire est comme une table de hachage en Perl. En Perl, les variables qui stockent des tables de hachage débutent toujours par le caractère %. En Python vous pouvez nommer votre variable comme bon vous semble et Python se chargera de la gestion du typage. NOTE: Python vs. Java: les dictionnaires Un dictionnaire Python est similaire à une instance de la classe Hashtable en Java. NOTE: Python vs. Visual Basic: les dictionnaires Un dictionnaire Python est similaire à une instance de l'objet Scripting.Dictionnary en Visual Basic. 3.1.1. Définition des dictionnaires Exemple 3.1. Définition d'un dictionnaire >>> d = {"server":"mpilgrim", "database":"master"} (1) >>> d {'server': 'mpilgrim', 'database': 'master'} >>> d["server"] (2) 'mpilgrim' >>> d["database"] (3) 'master' >>> d["mpilgrim"] (4) Traceback (innermost last): File "", line 1, in ? KeyError: mpilgrim (1) D'abord, nous créons un nouveau dictionnaire avec deux éléments que nous assignons à la variable d. Chaque élément est une paire clé-valeur et l'ensemble complet des éléments est entouré d'accolades. (2) 'server' est une clé et sa valeur associée, référencée par d["server"], est 'mpilgrim'. (3) 'database' est une clé et sa valeur associée, référencée par d["database"], est 'master'. (4) Vous pouvez obtenir les valeurs par clé, mais pas les clés à partir de leur valeur. Donc d["server"] est 'mpilgrim', mais d["mpilgrim"] déclenche une exception car 'mpilgrim' n'est pas une clé. 3.1.2. Modification des dictionnaires Exemple 3.2. Modification d'un dictionnaire >>> d {'server': 'mpilgrim', 'database': 'master'} >>> d["database"] = "pubs" (1) >>> d {'server': 'mpilgrim', 'database': 'pubs'} >>> d["uid"] = "sa" (2) >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'} (1) Vous ne pouvez avoir de clés dupliquées dans un dictionnaire. L'assignation d'une valeur à une clé existante a pour effet d'effacer l'ancienne valeur. (2) Vous pouvez ajouter de nouvelles paires clé-valeur à tout moment. La syntaxe est identique à celle utilisée pour modifier les valeurs existantes. (Oui, cela vous posera problème si vous essayez d'ajouter de nouvelles valeurs alors que vous ne faites que modifier constamment la même valeur parce que votre clé n'a pas changé de la manière que vous espériez) Notez que le nouvel élément (clé 'uid', valeur 'sa') à l'air d'être au milieu. En fait c'est par coïncidence que les éléments avaient l'air d'être dans l'ordre dans le premier exemple, c'est tout autant une coïncidence qu'ils aient l'air dans le désordre maintenant. NOTE: Les dictionnaires ne sont pas ordonnés Les dictionnaires ne sont liés à aucun concept d'ordonnancement des éléments. Il est incorrect de dire que les élément sont <>, il ne sont tout simplement pas ordonnés. C'est une distinction importante qui vous ennuiera lorsque vous souhaiterez accéder aux éléments d'un dictionnaire d'une façon spécifique et reproductible (par exemple par ordre alphabétique des clés). C'est possible, mais cette fonctionalite n'est pas integrée au dictionnaire. Quand vous utilisez des dictionnaires, vous devez garder à l'esprit le fait que les clés sont sensibles à la casse. Exemple 3.3. Les clés des dictionnaires sont sensibles à la casse >>> d = {} >>> d["key"] = "value" >>> d["key"] = "other value" (1) >>> d {'key': 'other value'} >>> d["Key"] = "third value" (2) >>> d {'Key': 'third value', 'key': 'other value'} (1) Assigner une valeur a une clé existante remplace l'ancienne valeur par la nouvelle. (2) Ici la valeur n'est pas assignée à une clé existante parce que les chaînes en Python sont sensibles à la casse, donc 'key' n'est pas la même chose que 'Key'. Une nouvelle paire clé/valeur est donc créée dans le dictionnaire, elle peut vous sembler similaire à la précédente, mais pour Python elle est complètement différente. Exemple 3.4. Mélange de types de données dans un dictionnaire >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'pubs'} >>> d["retrycount"] = 3 (1) >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 'retrycount': 3} >>> d[42] = "douglas" (2) >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 42: 'douglas', 'retrycount': 3} (1) Les dictionnaires ne servent pas uniquement aux chaînes de caractères. Les valeurs d'un dictionnaire peuvent être de n'importe quel type de données, y compris des chaînes, des entiers, des objets et même d'autres dictionnaires. Au sein d'un même dictionnaire les valeurs ne sont pas forcémment d'un même type, vous pouvez les mélanger à votre guise. (2) Les clés d'un dictionnaire sont plus restrictives, mais elles peuvent être des chaînes, des entiers et de quelques autres types encore (nous verrons cela en détail plus tard). Vous pouvez également mélanger divers types de données au sein des clés d'un dictionnaire. 3.1.3. Enlever des éléments d'un dictionnaire Exemple 3.5. Enlever des éléments d'un dictionnaire >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 42: 'douglas', 'retrycount': 3} >>> del d[42] (1) >>> d {'server': 'mpilgrim', 'uid': 'sa', 'database': 'master', 'retrycount': 3} >>> d.clear() (2) >>> d {} (1) L'instruction del vous permet d'effacer des éléments d'un dictionnaire en fonction de leur clé. (2) La méthode clear efface tous les éléments d'un dictionnaire. Notez que l'ensemble fait d'accolades vides signifie un dictionnaire sans éléments. Pour en savoir plus sur les dictionnaires • How to Think Like a Computer Scientist (http://www.ibiblio.org/obp/ thinkCSpy/) explique comment utiliser les dictionnaires pour modéliser les matrices creuses (http://www.ibiblio.org/obp/thinkCSpy/chap10.htm). • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) a de nombreux exemples de code ayant recours aux dictionnaires ( http://www.faqts.com/knowledge-base/index.phtml/fid/541). • Le Python Cookbook (http://www.activestate.com/ASPN/Python/Cookbook/) explique comment trier les valeurs d'un dictionnaire par leurs clés (http:/ /www.activestate.com/ASPN/Python/Cookbook/Recipe/52306). • La Python Library Reference (http://www.python.org/doc/current/lib/) résume toutes les méthodes des dictionnaires (http://www.python.org/doc/current/ lib/typesmapping.html). 3.2. Présentation des listes Les listes sont le type de données à tout faire de Python. Si votre seule expérience des listes sont les tableaux de Visual Basic ou (à Dieu ne plaise) les datastores de Powerbuilder, accrochez-vous pour les listes Python. NOTE: Python vs. Perl : listes Une liste en Python est comme un tableau Perl. En Perl, les variables qui stockent des tableaux débutent toujours par le caractère @, en Python vous pouvez nommer votre variable comme bon vous semble et Python se chargera de la gestion du typage. NOTE: Python vs. Java : listes Une liste Python est bien plus qu'un tableau en Java (même s'il peut être utilisé comme tel si vous n'attendez vraiment rien de mieux de la vie). Une meilleure analogie serait la classe ArrayList, qui peut contenir n'importe quels objets et qui croît dynamiquement au fur et à mesure que de nouveaux éléments y sont ajoutés. 3.2.1. Definition d'une liste Exemple 3.6. Definition d'une liste >>> li = ["a", "b", "mpilgrim", "z", "example"] (1) >>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[0] (2) 'a' >>> li[4] (3) 'example' (1) Premièrement, nous définissons une liste de 5 éléments. Notez qu'ils conservent leur ordre d'origine. Ce n'est pas un accident. Une liste est un ensemble ordonné d'éléments entouré par des crochets. (2) Une liste peut être utilisée comme un tableau dont l'indice de base est zéro. Le premier élément de toute liste non vide est toujours li[0]. (3) Le dernier élément de cette liste de 5 éléments est li[4] car les listes sont toujours indicées à partir de zéro. Exemple 3.7. Indices de liste négatifs >>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[-1] (1) 'example' >>> li[-3] (2) 'mpilgrim' (1) Un indice négatif permet d'accéder aux éléments à partir de la fin de la liste en comptant à rebours. Le dernier élément de toute liste non vide est toujours li[-1]. (2) Si vous trouvez que les indices négatifs prêtent à confusion, voyez-les comme suit : li[n] == li[n - len(li)]. Donc dans cette liste, li[-3] == li [5 - 3] == li[2]. Exemple 3.8. Découpage d'une liste >>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[1:3] (1) ['b', 'mpilgrim'] >>> li[1:-1] (2) ['b', 'mpilgrim', 'z'] >>> li[0:3] (3) ['a', 'b', 'mpilgrim'] (1) Vous pouvez obtenir un sous-ensemble d'une liste, appelé une <> (slice), en spécifiant deux indices. La valeur de retour est une nouvelle liste contenant les éléments de la liste, dans l'ordre, en démarrant du premier indice de la tranche (dans ce cas li[1]), jusqu'à au second indice de la tranche non inclu (ici li[3]). (2) Le découpage fonctionne si un ou les deux indices sont négatifs. Pour vous aider, vous pouvez les voir commer ceci : en lisant la liste de gauche à droite, le premier indice spécifie le premier élément que vous désirez et le second indice spécifie le premier élément dont vous ne voulez pas. La valeur de retour est tout ce qui se trouve entre les deux. (3) Les listes sont indicées à partir de zéro, donc li[0:3] retourne les trois premiers éléments de la liste, en démarrant à li[0] jusqu'à li[3] non inclu. Exemple 3.9. Raccourci pour le découpage >>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li[:3] (1) ['a', 'b', 'mpilgrim'] >>> li[3:] (2) (3) ['z', 'example'] >>> li[:] (4) ['a', 'b', 'mpilgrim', 'z', 'example'] (1) Si l'indice de tranche de gauche est 0, vous pouvez l'omettre et 0 sera implicite. Donc li[:3] est la même chose que li[0:3] dans le premier exemple. (2) De la même manière, si l'indice de tranche de droite est la longueur de la liste, vous pouvez l'omettre. Donc li[3:] est pareil que li[3:5], puisque la liste a 5 éléments. (3) Remarquez la symétrie. Dans cette liste de 5 éléments, li[:3] retourne les 3 premiers éléments et li[3:] retourne les deux derniers. En fait li[:n] retournera toujours les n premiers éléments et li[n:] le reste, quelle que soit la longueur de la liste. (4) Si les deux indices sont omis, tous les éléments de la liste sont inclus dans la tranche. Mais ce n'est pas la même chose que la liste li; c'est une nouvelle liste qui contient les même éléments. li[:] est un raccourci permettant d'effectuer une copie complète de la liste. 3.2.2. Ajout d'éléments à une liste Exemple 3.10. Ajout d'éléments à une liste >>> li ['a', 'b', 'mpilgrim', 'z', 'example'] >>> li.append("new") (1) >>> li ['a', 'b', 'mpilgrim', 'z', 'example', 'new'] >>> li.insert(2, "new") (2) >>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new'] >>> li.extend(["two", "elements"]) (3) >>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements'] (1) append ajoute un élément à la fin de la liste. (2) insert insère un élément dans la liste. L'argument numérique est l'indice du premier élément qui sera décalé. Notez que les éléments de la liste ne sont pas obligatoirement uniques ; il y a maintenant 2 éléments distincts avec la valeur 'new', li[2] and li[6]. (3) extend concatène des listes. Notez que vous n'appelez pas extend avec plusieurs arguments mais bien avec un seul argument qui est une liste. Dans le cas présent, la liste est composée de deux éléments. Exemple 3.11. Différence entre extend et append >>> li = ['a', 'b', 'c'] >>> li.extend(['d', 'e', 'f']) (1) >>> li ['a', 'b', 'c', 'd', 'e', 'f'] >>> len(li) (2) 6 >>> li[-1] 'f' >>> li = ['a', 'b', 'c'] >>> li.append(['d', 'e', 'f']) (3) >>> li ['a', 'b', 'c', ['d', 'e', 'f']] >>> len(li) (4) 4 >>> li[-1] ['d', 'e', 'f'] (1) Les listes ont deux méthodes, extend et append, qui semblent faire la même chose, mais sont en fait complètement différentes. extend prend un seul argument, qui est toujours une liste et ajoute chacun des éléments de cette liste à la liste originelle. (2) Ici nous avons une liste de trois éléments ('a', 'b' et 'c') et nous utilisons extended pour lui ajouter une liste de trois autres éléments ('d', 'e' et 'f'), ce qui nous donne une liste de six éléments. (3) Par contre, append prend un argument, qui peut être de n'importe quel type et l'ajoute simplement à la fin de la liste. Ici, nous appelons append avec un argument, qui est une liste de trois éléments. (4) Maintenant, la liste originelle qui avait trois éléments en contient quatre. Pourquoi quatre ? Parce que le dernier élément que nous venons d'ajouter est lui-même une liste. Les listes peuvent contenir n'importe quel type de données, y compris d'autres listes. En fonction du but recherché, faites attention de ne pas utiliser append si vous pensez en fait à extend. 3.2.3. Recherche dans une liste Exemple 3.12. Recherche dans une liste >>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements'] >>> li.index("example") (1) 5 >>> li.index("new") (2) 2 >>> li.index("c") (3) Traceback (innermost last): File "", line 1, in ? ValueError: list.index(x): x not in list >>> "c" in li (4) False (1) index trouve la première occurrence d'une valeur dans la liste et retourne son indice. (2) index trouve la première occurrence d'une valeur dans la liste. Dans ce cas, new apparaît à deux reprises dans la liste, li[2] et li[6], mais index ne retourne que le premier indice, 2. (3) Si la valeur est introuvable dans la liste, Python déclenche une exception. C'est sensiblement différent de la plupart des autres langages qui retournent un indice invalide. Si cela peut sembler gênant, c'est en fait une bonne chose car cela signifie que votre programme se plantera à la source même du problème plutôt qu'au moment ou vous tenterez de manipuler l'indice non valide. (4) Pour tester la présence d'une valeur dans la liste, utilisez in, qui retourne True si la valeur a été trouvée et False dans le cas contraire. NOTE: Qu'est-ce qui est vrai en Python ? Avant la version 2.2.1, Python n'avait pas de type booléen. Pour compenser cela, Python acceptait pratiquement n'importe quoi dans un contexte requérant un booléen (comme une instruction if), en fonction des règles suivantes : □ 0 est faux, tous les autres nombres sont vrai. □ Une chaîne vide ("") est faux, toutes les autres chaînes sont vrai. □ Une liste vide ([]) est faux, toutes les autres listes sont vrai. □ Un tuple vide (()) est faux, tous les autres tuples sont vrai. □ Un dictionnaire vide ({}) est faux, tous les autres dictionnaires sont vrai. Ces règles sont toujours valides en Python 2.3.3 et au-delà, mais vous pouvez maintenant utiliser un véritable booléen, qui a pour valeur True ou False. Notez la majuscule, ces valeurs comme tout le reste en Python, sont sensibles à la casse. 3.2.4. Suppression d'éléments d'une liste Exemple 3.13. Enlever des éléments d'une liste >>> li ['a', 'b', 'new', 'mpilgrim', 'z', 'example', 'new', 'two', 'elements'] >>> li.remove("z") (1) >>> li ['a', 'b', 'new', 'mpilgrim', 'example', 'new', 'two', 'elements'] >>> li.remove("new") (2) >>> li ['a', 'b', 'mpilgrim', 'example', 'new', 'two', 'elements'] >>> li.remove("c") (3) Traceback (innermost last): File "", line 1, in ? ValueError: list.remove(x): x not in list >>> li.pop() (4) 'elements' >>> li ['a', 'b', 'mpilgrim', 'example', 'new', 'two'] (1) remove enlève la première occurrence de la valeur de la liste. (2) remove enlève uniquement la première occurence de la valeur. Dans ce cas, new apparaît à deux reprises dans la liste mais li.remove("new") a seulement retiré la première occurrence. (3) Si la valeur est introuvable dans la liste, Python déclenche une exception. Ce comportement est identique à celui de la méthode index. (4) pop est un spécimen intéressant. Il fait deux choses : il enlève le dernier élément de la liste et il retourne la valeur qui a été enlevé. Notez que cela diffère de li[-1] qui retourne une valeur mais ne modifie pas la liste et de li.remove(valeur) qui altère la liste mais ne retourne pas de valeur. 3.2.5. Utilisation des opérateurs de listes Exemple 3.14. Opérateurs de listes >>> li = ['a', 'b', 'mpilgrim'] >>> li = li + ['example', 'new'] (1) >>> li ['a', 'b', 'mpilgrim', 'example', 'new'] >>> li += ['two'] (2) >>> li ['a', 'b', 'mpilgrim', 'example', 'new', 'two'] >>> li = [1, 2] * 3 (3) >>> li [1, 2, 1, 2, 1, 2] (1) Les listes peuvent être concaténées à l'aide de l'opérateur +. liste = liste + autreliste est équivalent à list.extend(autreliste). Mais l'opérateur + retourne une nouvelle liste concaténée comme une valeur alors que extend modifie une liste existante. Cela implique que extend est plus rapide, surtout pour de grandes listes. (2) Python supporte l'opérateur +=. li += ['two'] est équivalent à li = li + ['two']. L'opérateur += fonctionne pour les listes, les chaînes et les entiers. Il peut être surchargé pour fonctionner également avec des classes définies par l'utilisateur (nous en apprendrons plus sur les classes au Chapitre 5). (3) L'opérateur * agit sur les liste comme un répéteur. li = [1, 2] * 3 est équivalent à li = [1, 2] + [1, 2] + [1, 2], qui concatène les trois listes en une seule. Pour en savoir plus sur les listes • How to Think Like a Computer Scientist (http://www.ibiblio.org/obp/ thinkCSpy/) explique les listes et expose le sujet important du passage de listes comme arguments de fonction (http://www.ibiblio.org/obp/thinkCSpy/ chap08.htm). • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) montre comment utiliser des listes comme des piles ou des files (http:// www.python.org/doc/current/tut/node7.html#SECTION007110000000000000000). • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) répond aux questions courantes à propos des listes (http:// www.faqts.com/knowledge-base/index.phtml/fid/534) et fourni de nombreux exemples de code utilisant des listes (http://www.faqts.com/knowledge-base/ index.phtml/fid/540). • La Python Library Reference (http://www.python.org/doc/current/lib/) résume toutes les méthodes des listes (http://www.python.org/doc/current/lib/ typesseq-mutable.html). 3.3. Présentation des tuples Un tuple (n-uplet) est une liste non-mutable. Un fois créé, un tuple ne peut en aucune manière être modifié. Exemple 3.15. Définition d'un tuple >>> t = ("a", "b", "mpilgrim", "z", "example") (1) >>> t ('a', 'b', 'mpilgrim', 'z', 'example') >>> t[0] (2) 'a' >>> t[-1] (3) 'example' >>> t[1:3] (4) ('b', 'mpilgrim') (1) Un tuple est défini de la même manière qu'une liste sauf que l'ensemble d'éléments est entouré de parenthèses plutôt que de crochets. (2) Les éléments d'un tuple ont un ordre défini, tout comme ceux d'une liste. Les indices de tuples débutent à zéro, tout comme ceux d'une liste, le premier élément d'un tuple non vide est toujours t[0]. (3) Les indices négatifs comptent à partir du dernier élément du tuple, tout comme pour une liste. (4) Le découpage fonctionne aussi, tout comme pour une liste. Notez que lorsque vous découpez une liste, vous obtenez une nouvelle liste, lorsque vous découpez un tuple, vous obtenez un nouveau tuple. Exemple 3.16. Les tuples n'ont pas de méthodes >>> t ('a', 'b', 'mpilgrim', 'z', 'example') >>> t.append("new") (1) Traceback (innermost last): File "", line 1, in ? AttributeError: 'tuple' object has no attribute 'append' >>> t.remove("z") (2) Traceback (innermost last): File "", line 1, in ? AttributeError: 'tuple' object has no attribute 'remove' >>> t.index("example") (3) Traceback (innermost last): File "", line 1, in ? AttributeError: 'tuple' object has no attribute 'index' >>> "z" in t (4) True (1) Vous ne pouvez pas ajouter d'élément à un tuple. Les tuples n'ont pas de méthodes append ou extend. (2) Vous ne pouvez pas enlever d'éléments d'un tuple. Les tuples n'ont pas de méthodes remove ou pop. (3) Vous ne pouvez pas rechercher d'éléments dans un tuple. Les tuples n'ont pas de méthode index. (4) Vous pouvez toutefois utiliser in pour vérifier l'existence d'un élément dans un tuple. Mais à quoi servent donc les tuples ? • Les tuples sont plus rapides que les listes. Si vous définissez un ensemble constant de valeurs et que tout ce que vous allez faire est le parcourir, utilisez un tuple au lieu d'une liste. • Votre code est plus sûr si vous <> les données qui n'ont pas besoin d'être modifiées. Utiliser un tuple à la place d'une liste revient à avoir une assertion implicite que les données sont constantes et que des mesures spécifiques sont nécéssaires pour modifier cette définition. • Vous vous souvenez que j'avais dit que que les clés de dictionnaire pouvaient être des entiers, des chaînes et <> ? Les tuples sont un de ces types. Ils peuvent être utilisé comme clé dans un dictionnaire, ce qui n'est pas le cas des listes. En fait, c'est plus compliqué que ça. Les clés de dictionnaire doivent être non-mutables. Les tuples sont non-mutables mais si vous avez un tuple contenant des listes, il est considéré comme mutable et n'est pas utilisable comme clé de dictionnaire. Seuls les tuples de chaînes, de nombres ou d'autres tuples utilisable comme clé peuvent être utilisés comme clé de dictionnaire. • Les tuples sont utilisés pour le formatage de chaînes, comme nous le verrons bientôt. NOTE: De tuple à liste à tuple Les tuples peuvent être convertis en listes et vice-versa. La fonction prédéfinie tuple prends une liste et retourne un tuple contenant les mêmes éléments et la fonction list prends un tuple et retourne une liste. En fait, tuple gèle une liste et list dégèle un tuple. Pour en savoir plus sur les tuples • How to Think Like a Computer Scientist (http://www.ibiblio.org/obp/ thinkCSpy/) explique les tuples et montre comment concaténer des tuples ( http://www.ibiblio.org/obp/thinkCSpy/chap10.htm). • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) vous apprendra à trier un tuple (http://www.faqts.com/ knowledge-base/view.phtml/aid/4553/fid/587). • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) explique comment définir un tuple avec un seul élément (http:// www.python.org/doc/current/tut/node7.html#SECTION007300000000000000000). 3.4. Définitions de variables Maintenant que vous pensez tout savoir à propos des dictionnaires, des tuples et des listes (hum!), revenons à notre programme d'exemple du Chapitre 2, odbchelper.py. Python dispose de variables locales et globales comme la plupart des autres langages, mais il n'a pas de déclaration explicite des variables. Les variables viennent au monde en se voyant assigner une valeur et sont automatiquement détruites lorsqu'elles se retrouvent hors de portée. Exemple 3.17. Définition de la variable myParams if __name__ == "__main__": myParams = {"server":"mpilgrim", \ "database":"master", \ "uid":"sa", \ "pwd":"secret" \ } Il y a plusieurs points intéressants ici. Tout d'abord, notez l'indentation. Une instruction if est un bloc de code et nécessite d'être indenté tout comme une fonction. Deuxièmement, l'assignation de variable est une commande étalée sur plusieurs lignes avec une barre oblique (<<\>>) servant de marque de continuation de ligne. NOTE: Commandes multilignes Lorsq'une commande est étalée sur plusieurs lignes avec le marqueur de continuation de ligne (<<\>>), les lignes suivantes peuvent être indentées de n'importe qu'elle manière, les règles d'indentation strictes habituellement utilisées en Python ne s'appliquent pas. Si votre IDE Python indente automatiquement les lignes continuées, vous devriez accepter ses réglages par défauts sauf raison impérative. Les expressions entre parenthèses, crochets ou accolades (comme la définition d'un dictionnaire) peuvent être réparties sur plusieurs lignes avec ou sans le caractère de continuation (<<\>>). Je préfère inclure la barre oblique même lorsqu'elle n'est pas requise car je pense que cela rends le code plus lisible mais c'est une question de style. Troisièmement, vous n'avez jamais déclaré la variable myParams, vous lui avez simplement assigné une valeur. C'est comme en VBScript sans l'option option explicit. Heureusement, à l'inverse de VBScript, Python ne permet pas de référencer une variable à laquelle aucune valeur n'a été assigné. Tenter de le faire déclenchera une exception. 3.4.1. Référencer des variables Exemple 3.18. Référencer une variable non assignée >>> x Traceback (innermost last): File "", line 1, in ? NameError: There is no variable named 'x' >>> x = 1 >>> x 1 Un jour, vous remercierez Python pour ça. 3.4.2. Assignation simultanée de plusieurs valeurs Un des raccourcis les plus réjouissants existant en Python est l'utilisation de séquences pour assigner plusieurs valeurs en une fois. Exemple 3.19. Assignation simultanée de plusieurs valeurs >>> v = ('a', 'b', 'e') >>> (x, y, z) = v (1) >>> x 'a' >>> y 'b' >>> z 'e' (1) v est un tuple de trois éléments et (x, y, z) est un tuple de trois variables. Le fait d'assigner l'un à l'autre assigne chacune des valeurs de v a chacune des variables, dans leur ordre respectif. Ce type d'assignation a de multiples usages. Je souhaite souvent assigner des noms a une série de valeurs. En C, vous utiliseriez enum et vous listeriez manuellement chaque constante et la valeur associée, ce qui semble particulièrement fastidieux lorsque les valeurs sont consécutives. En Python, vous pouvez utiliser la fonction prédéfinie range avec l'assignation multiples de variables pour assigner rapidement des valeurs consécutives. Exemple 3.20. Assignation de valeurs consécutives >>> range(7) (1) [0, 1, 2, 3, 4, 5, 6] >>> (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7) (2) >>> MONDAY (3) 0 >>> TUESDAY 1 >>> SUNDAY 6 (1) La fonction prédéfinie range retourne une liste d'entiers. Dans sa forme la plus simple, elle prends une borne supérieure et retourne une séquence démarrant à 0 mais n'incluant pas la borne supérieure. (Si vous le souhaitez, vous pouvez spécifier une borne inférieure différente de 0 ou un pas d'incrément différent de 1. Vous pouvez faire un print range.__doc__ pour de plus amples détails.) (2) MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY et SUNDAY sont les variables que nous définissons (cet exemple provient du module calendar, qui est un petit module amusant qui affiche des calendriers comme le programme cal sous UNIX. Le module calendar défini des constantes entières pour les jours de la semaine). (3) A présent, chaque variable possède sa valeur : MONDAY vaut 0, TUESDAY vaut 1 et ainsi de suite. Vous pouvez aussi utiliser l'assignation multiple pour créer des fonctions qui retournent plusieurs valeurs, simplement en retournant un tuple contenant ces valeurs. L'appelant peut le traiter en tant que tuple ou assigner les valeurs à différentes variables. Beaucoup de bibliothèques standard de Python font cela, y compris le module os dont nous traiterons au Chapitre 6. Pour en savoir plus sur les variables • Le Python Reference Manual (http://www.python.org/doc/current/ref/) présente des exemples des cas où vous pouvez omettre le marqueur de continuation (http://www.python.org/doc/current/ref/implicit-joining.html) et où vous devez l'utiliser (http://www.python.org/doc/current/ref/ explicit-joining.html). • How to Think Like a Computer Scientist (http://www.ibiblio.org/obp/ thinkCSpy/) montre comment utiliser l'assignation multiple pour échanger les valeurs de deux variables (http://www.ibiblio.org/obp/thinkCSpy/ chap09.htm). 3.5. Formatage de chaînes Python supporte le formatage de valeurs en chaînes de caractères. Bien que cela peut comprendre des expression très compliquées, l'usage le plus simple consiste à insérer des valeurs dans des chaînes à l'aide de marques %s. NOTE: Python vs. C : formatage de chaînes Le formatage de chaînes en Python utilise la même syntaxe que la fonction C sprintf. Exemple 3.21. Présentation du formatage de chaînes >>> k = "uid" >>> v = "sa" >>> "%s=%s" % (k, v) (1) 'uid=sa' (1) L'expression entière est évaluée en chaîne. Le premier %s est remplacé par la valeur de k, le second %s est remplacé par la valeur de v. Tous les autres caractères de la chaînes (le signe d'égalité dans le cas présent) restent tels quels. Notez que (k, v) est un tuple. Je vous avais dit qu'ils servaient à quelque chose. Vous pourriez pensez que cela représente beaucoup d'efforts pour le formatage de chaîne se bornait à la concaténation. Il n'y est pas question uniquement de formatage mais également de conversion de types. Exemple 3.22. Formatage de chaîne et concaténation >>> uid = "sa" >>> pwd = "secret" >>> print pwd + " is not a good password for " + uid (1) secret is not a good password for sa >>> print "%s is not a good password for %s" % (pwd, uid) (2) secret is not a good password for sa >>> userCount = 6 >>> print "Users connected: %d" % (userCount, ) (3) (4) Users connected: 6 >>> print "Users connected: " + userCount (5) Traceback (innermost last): File "", line 1, in ? TypeError: cannot concatenate 'str' and 'int' objects (1) + est l'opérateur de concaténation de chaînes. (2) Dans ce cas trivial, le formatage de chaînes mène au même résultat que la concaténation. (3) (userCount, ) est un tuple contenant un seul élément. Oui, la syntaxe est un peu étrange mais il y a un excellente raison : c'est un tuple sans ambiguité aucune. En fait, vous pouvez toujours mettre une virgule après l'élément terminal lors de la définition d'une liste, d'un tuple ou d'un dictionnaire mais cette virgule est obligatoire lors de la définition d'un tuple avec un élément unique. Si ce n'était pas le cas, Python ne pourrait distinguer si (userCount) est un tuple avec un seul élément ou juste la valeur userCount. (4) Le formatage de chaîne fonctionne également avec des entiers en spécifiant %d au lieu de %s. (5) Si vous tentez de concaténer une chaîne avec un autre type, Python va déclencher une exception. Au contraire du formatage de chaîne, la concaténation ne fonctionne que si tout les objets sont déjà de type chaîne. Comme la fonction printf en C, le formatage de chaînes en Python est un véritable couteau suisse. Il y a des options à profusion et des modificateurs de format spécifiques pour de nombreux types de valeurs. Exemple 3.23. Formatage de nombres >>> print "Today's stock price: %f" % 50.4625 (1) 50.462500 >>> print "Today's stock price: %.2f" % 50.4625 (2) 50.46 >>> print "Change since yesterday: %+.2f" % 1.5 (3) +1.50 (1) L'option de formatage %f considère la valeur comme un nombre décimal et l'affiche avec six chiffres après la virgule. (2) Le modificateurs ".2" de l'option %f tronque la valeur à deux chiffres après la virgule. (3) On peut également combiner les modificateurs. Ajouter le modificateur + affiche le signe positif ou négatif avant la valeur. Notez que le modificateur ".2" est toujours en place et qu'il formate la valeur avec exactement deux chiffres après la virgule. Pour en savoir plus sur le formatage de liste • La Python Library Reference (http://www.python.org/doc/current/lib/) résume tous les caractères spéciaux utilisés pour le formatage de chaînes (http:// www.python.org/doc/current/lib/typesseq-strings.html). • Effective AWK Programming (http://www-gnats.gnu.org:8080/cgi-bin/info2www? (gawk)Top) explique tous les caractères de formatage (http:// www-gnats.gnu.org:8080/cgi-bin/info2www?(gawk)Control+Letters) et des technique de formatage avancées comme le règlage de la largeur ou de la précision et le remplissage avec des zéros (http://www-gnats.gnu.org:8080/ cgi-bin/info2www?(gawk)Format+Modifiers). 3.6. Mutation de listes Une des caractéristiques les plus puissantes de Python est la list comprehension (création fonctionnelle de listes) qui fournit un moyen concis d'appliquer une fonction sur chaque élément d'une liste afin d'en produire une nouvelle. Exemple 3.24. Présentation des list comprehensions >>> li = [1, 9, 8, 4] >>> [elem*2 for elem in li] (1) [2, 18, 16, 8] >>> li (2) [1, 9, 8, 4] >>> li = [elem*2 for elem in li] (3) >>> li [2, 18, 16, 8] (1) Pour comprendre cette ligne, observez là de droite à gauche. li est la liste que vous appliquez. Python la parcourt un élément à la fois, en assignant temporairement la valeur de chacun des éléments à la variable elem. Python applique ensuite la fonction elem*2 et ajoute le résultat à la liste retournée. (2) Notez que les list comprehensions ne modifient pas la liste initiale. (3) Vous pouvez assigner le résultat d'une list comprehension à la variable que vous traitez. Python assemble la nouvelle liste en mémoire et assigne le résultat à la variable une fois la transformation terminée. Voici les list comprehensions dans la fonction buildConnectionString que nous avons déclaré au Chapitre 2: ["%s=%s" % (k, v) for k, v in params.items()] Notez tout d'abord que vous appelez la fonction items du dictionnaire params. Cette fonction retourne une liste de tuples avec toutes les données stockées dans le dictionnaire. Exemple 3.25. Les fonctions keys, values et items >>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> params.keys() (1) ['server', 'uid', 'database', 'pwd'] >>> params.values() (2) ['mpilgrim', 'sa', 'master', 'secret'] >>> params.items() (3) [('server', 'mpilgrim'), ('uid', 'sa'), ('database', 'master'), ('pwd', 'secret')] (1) La méthode keys d'un dictionnaire retourne la liste de toutes les clés. Cette liste ne suit pas l'ordre dans lequel le dictionnaire a été défini (souvenez-vous, les éléments d'un dictionnaire ne sont pas ordonnés) mais cela reste une liste. (2) La méthode values retourne la liste de toutes les valeurs. La liste est dans le même ordre que celle retournée parkeys, on a donc params.values() [n] == params[params.keys[n]] pour toute valeur de n. (3) La méthode items retourne une liste de tuples de la forme (clé, valeur). La liste contient toutes les données stockées dans le dictionnaire. Voyons maintenant ce que fait buildConnectionString. Elle prends une liste, param.items(), et crée une nouvelle liste en appliquant une instruction de formatage de chaîne à chacun de ses éléments. La nouvelle liste aura le même nombre d'éléments que params.items() mais chaque élément sera une chaîne qui contient à la fois une clé et la valeur qui lui est associée dans le dictionnaire params. Exemple 3.26. List comprehensions dans buildConnectionString, pas à pas >>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> params.items() [('server', 'mpilgrim'), ('uid', 'sa'), ('database', 'master'), ('pwd', 'secret')] >>> [k for k, v in params.items()] (1) ['server', 'uid', 'database', 'pwd'] >>> [v for k, v in params.items()] (2) ['mpilgrim', 'sa', 'master', 'secret'] >>> ["%s=%s" % (k, v) for k, v in params.items()] (3) ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret'] (1) Notez que nous utilisons deux variables pour parcourir la liste params.items(). Il s'agit d'un autre usage de l'assignment multiple. Le premier élément de params.items() est ('server', 'mpilgrim'), donc lors de la première itération de la transformation, k va prendre la valeur 'server' et v la valeur 'mpilgrim'. Dans ce cas, nous ignorons la valeur de v et placons uniquement la valeur de k dans la liste résultante. Cette transformation correspond donc au comportement de params.keys(). (Vous n'utiliseriez pas réellement une list comprehension comme ceci dans du vrai code; il s'agit d'un exemple exagérément simple pour que vous compreniez ce qui se passe.) (2) Nous faisons la même chose ici, mais nous ignorons la valeur de k de telle sorte que le résultat est équivalent à celui de params.values(). (3) En combinant les deux exemples précédent avec le formatage de chaîne, nous obtenons une liste de chaînes comprenant la clé et la valeur de chaque élément du dictionnaire. Cela ressemble étonnament à la sortie du programme, tout ce qui reste à faire maintenant est la jointure des éléments de cette liste en une seule chaîne. Pour en savoir plus sur les list comprehensions • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite d'une autre manière de transformer des listes avec la fonction prédéfinie map (http://www.python.org/doc/current/tut/node7.html# SECTION007130000000000000000). • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) montre comment emboîter des mutations de listes (http://www.python.org/doc/current /tut/node7.html#SECTION007140000000000000000). 3.7. Jointure de listes et découpage de chaînes Nous avons une liste de paires clé-valeur sous la forme clé=valeur et nous voulons les assembler au sein d'une même chaîne. Pour joindre une liste de chaînes en une seule, nous pouvons utiliser la méthode join d'un objet chaîne. Voici un exemple de jointure de liste provenant de la fonction buildConnectionString : return ";".join(["%s=%s" % (k, v) for k, v in params.items()]) Une remarque intéressante avant de continuer. Je ne cesse de répéter que les fonctions sont des objets, que les chaînes sont des objets, que tout est objet. Vous pourriez penser que seules les variables de type chaîne sont des objets. Mais ce n'est pas le cas, regardez de plus près cet exemple et vous verrez que la chaîne ";" est elle même un objet dont vous appelez la méthode join. La méthode join assemble les éléments d'une liste pour former une chaîne unique, chaque élément étant séparé par un point virgule. Le séparateur n'est pas forcément un point-virgule, il n'est même pas forcément un caractère unique. Il peut être n'importe quelle chaîne. Attention: Attention La méthode join ne fonctionne qu'avec des listes de chaînes; elle n'applique pas la conversion de types. La jointure d'une liste comprenant au moins un élément non-chaîne déclenchera une exception. Exemple 3.27. Sortie de odbchelper.py >>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"} >>> ["%s=%s" % (k, v) for k, v in params.items()] ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret'] >>> ";".join(["%s=%s" % (k, v) for k, v in params.items()]) 'server=mpilgrim;uid=sa;database=master;pwd=secret' La chaîne est alors retournée de la fonction odbchelper et affichée par le bloc appelant, ce qui vous donne la sortie qui vous a tant émerveillé quand vous avez débuté la lecture de ce chapitre. Vous vous demandez probablement s'il existe une méthode analogue permettant de découper une chaîne en liste. Et bien sur elle existe, elle porte le nom de split. Exemple 3.28. Découpage d'une chaîne >>> li = ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret'] >>> s = ";".join(li) >>> s 'server=mpilgrim;uid=sa;database=master;pwd=secret' >>> s.split(";") (1) ['server=mpilgrim', 'uid=sa', 'database=master', 'pwd=secret'] >>> s.split(";", 1) (2) ['server=mpilgrim', 'uid=sa;database=master;pwd=secret'] (1) split fait l'inverse de join en découpant une chaîne en une liste de plusieurs éléments. Notez que le délimiteur (<<;>>) est totalement supprimé, il n'apparaît dans aucun des éléments de la liste retournée. (2) split prend en deuxième argument optionnel le nombre de découpages à effectuer (<> Vous apprendrez à en définir dans vos propres fonctions au prochain chapitre.) ASTUCE: Rechercher avec split une_chaîne.split(delimiteur, 1) est une technique utile pour chercher une sous-chaîne dans une chaîne et utiliser tout ce qui précède cette sous-chaîne (le premier élément de la liste retournée) et tout ce qui la suit (le second élément de la liste retournée). Pour en savoir plus sur les méthodes de chaînes • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) répond aux questions courantes à propose des chaînes (http:// www.faqts.com/knowledge-base/index.phtml/fid/480) et dispose de nombreux exemples de code utilisant des chaînes (http://www.faqts.com/knowledge-base /index.phtml/fid/539). • La Python Library Reference (http://www.python.org/doc/current/lib/) récapitule toutes les méthodes de chaînes (http://www.python.org/doc/ current/lib/string-methods.html). • La Python Library Reference (http://www.python.org/doc/current/lib/) documente le module string (http://www.python.org/doc/current/lib/ module-string.html). • The Whole Python FAQ (http://www.python.org/doc/FAQ.html) explique pourquoi join est une méthode de chaînes (http://www.python.org/cgi-bin/faqw.py? query=4.96&querytype=simple&casefold=yes&req=search) et non une méthode de liste. 3.7.1. Note historique sur les méthodes de chaînes Lorsque j'ai débuté l'apprentissage de Python, je m'attendais à ce que join soit une méthode de liste qui aurait pris un séparateur comme argument. Beaucoup de gens pensent la même chose et il y a une véritable histoire derrière la méthode join. Avant Python 1.6, les chaînes n'étaient pas pourvue de toutes ces méthodes si utiles. Il y avait un module string séparé qui contenait toutes les fonctions de manipulation de chaînes de caractères, chacune prenant une chaîne comme premier argument. Ces fonctions ont été considérées assez importantes pour être intégrées dans les chaînes elles même, ce qui semblait logique pour des fonctions comme lower, upper et split. Mais beaucoup de programmeurs Python issus du noyau dur émirent des objections quant à la méthode join en arguant du fait qu'elle devrait être plutôt une méthode de liste ou tout simplement rester une fonction du module string (qui contient encore bien des choses utiles). J'utilise exclusivement la nouvelle méthode join mais vous verrez du code écrit des deux façons et si cela vous pose un réel problème, vous pouvez toujours opter pour l'ancienne fonction string.join. 3.8. Résumé A présent, le programme odbchelper.py et sa sortie devraient vous paraître parfaitement clairs. def buildConnectionString(params): """Build a connection string from a dictionary of parameters. Returns string.""" return ";".join(["%s=%s" % (k, v) for k, v in params.items()]) if __name__ == "__main__": myParams = {"server":"mpilgrim", \ "database":"master", \ "uid":"sa", \ "pwd":"secret" \ } print buildConnectionString(myParams) Voici la sortie de odbchelper.py: server=mpilgrim;uid=sa;database=master;pwd=secret Avant de vous plonger dans le chapitre suivant, assurez vous que vous vous sentez à l'aise pour : • Utiliser l'IDE Python pour tester des expressions de manière interactive • Ecrire des modules Python et les exécuter depuis votre IDE ou en ligne de commande • Importer des modules et appeler leurs fonctions • Déclarer des fonctions et utiliser des doc string, des variables locales et une indentation correcte • Définir des dictionnaires, des tuples et des listes • Accéder aux attributs et méthodes de tout objet, y compris les chaînes, les listes, les dictionnaires, les fonctions et les modules • Concaténer des valeur avec le formatage de chaînes • Utiliser les lists comprehensions pour la mutation de listes • Découper des chaînes en listes et joindre des listes en chaînes Chapitre 4. Le pouvoir de l’introspection Ce chapitre traite d’une des forces de Python : l’introspection. Comme vous le savez, tout est objet dans Python, l’introspection consiste à considérer des modules et des fonctions en mémoire comme des objets, à obtenir des informations de leur part et à les manipuler. Au cours du chapitre, nous définirons des fonctions sans nom, nous appelerons des fonctions avec les arguments dans le désordre et nous référencerons des fonctions dont nous ne connaissons même pas le nom à l’avance. 4.1. Plonger Voici un programme Python complet et fonctionnel. Vous devriez en comprendre une grande partie rien qu’en le lisant. Les lignes numérotées illustrent des concepts traités dans Chapitre 2, Votre premier programme Python. Ne vous inquiétez pas si le reste du code a l’air intimidant, vous en apprendrez tous les aspects au cours de ce chapitre. Exemple 4.1. apihelper.py Si vous ne l’avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples (http://diveintopython.org/download/ diveintopython-examples-5.4.zip) du livre. def info(object, spacing=10, collapse=1): (1) (2) (3) """Print methods and doc strings. Takes module, class, list, dictionary, or string.""" methodList = [method for method in dir(object) if callable(getattr(object, method))] processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s) print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList]) if __name__ == "__main__": (4) (5) print info.__doc__ (1) Ce module a une fonction, info. Selon sa déclaration de fonction, elle prend trois paramètres : object, spacing et collapse. Les deux derniers sont en fait des paramètres optionnels comme nous le verrons bientôt. (2) La fonction info a une doc string multi-lignes qui décrit succintement son usage. Notez qu’aucune valeur de retour n’est mentionnée, cette fonction sera employée uniquement pour son effet, pas sa valeur. (3) Le code à l’interieur de la fonction est indenté. (4) L’astuce if __name__ permet à ce programme de faire quelque chose d’utile lorsqu’il est exécuté tout seul sans intérférence avec son usage comme module pour d’autres programmes. Dans ce cas, le programme affiche simplement la doc string de la fonction info. (5) L’instruction if utilise == pour la comparaison et ne nécessite pas de parenthèses. La fonction info est conçue pour être utilisée par vous, le programmeur, lorsque vous travaillez dans l’IDE Python. Elle prend n’importe quel objet qui a des fonctions ou des méthodes (comme un module, qui a des fonction, ou une liste, qui a des méthodes) et affiche les fonctions et leur doc string. Exemple 4.2. Exemple d'utilisation de apihelper.py >>> from apihelper import info >>> li = [] >>> info(li) append L.append(object) -- append object to end count L.count(value) -> integer -- return number of occurrences of value extend L.extend(list) -- extend list by appending list elements index L.index(value) -> integer -- return index of first occurrence of value insert L.insert(index, object) -- insert object before index pop L.pop([index]) -> item -- remove and return item at index (default last) remove L.remove(value) -- remove first occurrence of value reverse L.reverse() -- reverse *IN PLACE* sort L.sort([cmpfunc]) -- sort *IN PLACE*; if given, cmpfunc(x, y) -> -1, 0, 1 Par défaut, la sortie est formatée pour être facilement lisible. Les doc string multi-lignes sont combinées en une seule longue ligne, mais cette option peut être changée en spécifiant 0 pour l’argument collapse. Si les noms de fonction font plus de 10 caractères, vous pouvez spécifier une valeur plus grande pour l’argument spacing, pour faciliter la lecture. Exemple 4.3. Utilisation avancée de apihelper.py >>> import odbchelper >>> info(odbchelper) buildConnectionString Build a connection string from a dictionary Returns string. >>> info(odbchelper, 30) buildConnectionString Build a connection string from a dictionary Returns string. >>> info(odbchelper, 30, 0) buildConnectionString Build a connection string from a dictionary Returns string. 4.2. Arguments optionnels et nommés Python permet aux arguments de fonction d’avoir une valeur par défaut, si la fonction est appelée sans l’argument il a la valeur par défaut. De plus, les arguments peuvent être donnés dans n’importe quel ordre en utilisant les arguments nommés. Les procédures stockées de Transact/SQL sous SQL Server peuvent faire la même chose, si vous êtes un as des scripts sous SQL Server, vous pouvez survoler cette partie. Voici un exemple de info, une fonction avec deux arguments optionnels : def info(object, spacing=10, collapse=1): spacing et collapse sont optionnels car ils ont des valeurs par défaut définies. object est obligatoire car il n’a pas de valeur par défaut. Si info est appelé avec un seul argument, spacing prend pour valeur 10 et collapse la valeur 1. Si info est appelé avec deux arguments, collapse prend encore pour valeur 1. Imaginez que vous vouliez spécifier une valeur pour collapse mais garder la valeur par défaut pour spacing. Dans la plupart des langages, vous ne pouvez pas le faire, vous auriez à spécifier les trois arguments. Mais en Python, les arguments peuvent être spécifiés par leur nom, dans n’importe quel ordre. Exemple 4.4. Appels de info autorisés info(odbchelper) (1) info(odbchelper, 12) (2) info(odbchelper, collapse=0) (3) info(spacing=15, object=odbchelper) (4) (1) Avec un seul argument, spacing prend pour valeur 10 et collapse 1. (2) Avec deux arguments, collapse prend pour valeur 1. (3) Ici, vous nommez l’argument collapse explicitement et spécifiez sa valeur. spacing prend la valeur par défaut 10. (4) Les arguments obligatoires (comme object, qui n’a pas de valeurs par défaut) peuvent aussi être nommés et les arguments nommés peuvent apparaître dans n’importe quel ordre. Cela a l’air confus jusqu’à que vous réalisiez que les arguments sont tout simplement un dictionnaire. La manière <> d’appeler les fonctions sans le nom des arguments est en fait un raccourci dans lequel Python fait correspondre les valeurs avec le nom des arguments dans l’ordre dans lequel ils sont spécifiés par la déclaration de fonction. Dans la plupart des cas, vous appellerez le fonctions de la manière <>, mais vous aurez toujours cette souplesse pour les autres cas. NOTE: L’appel de fonctions est souple La seule chose que vous avez à faire pour appeler une fonction est de spécifier une valeur (d’une manière ou d’une autre) pour chaque argument obligatoire, la manière et l’ordre dans lequel vous le faites ne dépendent que de vous. Pour en savoir plus • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite de manière précise de quand et comment les arguments par défaut sont évalués (http://www.python.org/doc/current/tut/node6.html# SECTION006710000000000000000), ce qui est important lorsque la valeur par défaut est une liste ou une expression ayant un effet de bord. 4.3. Utilisation de type, str, dir et autres fonction prédéfinies Python a un petit ensemble de fonctions prédéfinies très utiles. Toutes les autres fonctions sont réparties dans des modules. C’est une décision de conception consciente, afin d’éviter au langage de trop grossir comme d’autres langages de script (au hasard, Visual Basic). 4.3.1. La fonction type La fonction type retourne le type de données d’un objet quelconque. Les types possibles sont répertoriés dans le module types. C’est utile pour les fonctions capables de gérer plusieurs types de données. Exemple 4.5. Présentation de type >>> type(1) (1) >>> li = [] >>> type(li) (2) >>> import odbchelper >>> type(odbchelper) (3) >>> import types (4) >>> type(odbchelper) == types.ModuleType True (1) type prend n’importe quel argument et retourne son type de données. Je dis bien n’importe lequel : entiers, chaînes, listes, dictionnaires, tuples, fonctions, classes, modules et même types. (2) type peut prendre une variable et retourne son type de données. (3) type fonctionne aussi avec les modules. (4) Vous pouvez utiliser les constantes du module types pour comparer les types des objets. C’est ce que fait la fonction info, comme nous le verrons bientôt. 4.3.2. La fonction str La fonction str convertit des données en chaîne. Tous les types de données peuvent être convertis en chaîne. Exemple 4.6. Présentation de str >>> str(1) (1) '1' >>> horsemen = ['war', 'pestilence', 'famine'] >>> horsemen ['war', 'pestilence', 'famine'] >>> horsemen.append('Powerbuilder') >>> str(horsemen) (2) "['war', 'pestilence', 'famine', 'Powerbuilder']" >>> str(odbchelper) (3) "" >>> str(None) (4) 'None' (1) Pour des types simples comme les entiers, il semble normal que str fonctionne, presque tous les langages ont une fonction de conversion d’entier en chaîne. (2) Cependant, str fonctionne pour les objets de tout type. Ici, avec une liste que nous avons construit petit à petit. (3) str fonctionne aussi pour les modules. Notez que la représentation en chaîne du module comprend le chemin du module sur le disque, la votre sera donc différente. (4) Un aspect subtil mais important du comportement de str est qu’elle fonctionne pour None, la valeur nulle de Python. Elle retourne la chaîne 'None'. Nous utiliserons cela à notre avantage dans la fonction info, comme nous le verrons bientôt. Au coeur de notre fonction info, il y a la puissante fonction dir. dir retourne une liste des attributs et méthodes de n’importe quel objet : module, fonction, chaîne, liste, dictionnaire... à peu près tout. Exemple 4.7. Introducing dir >>> li = [] >>> dir(li) (1) ['append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'] >>> d = {} >>> dir(d) (2) ['clear', 'copy', 'get', 'has_key', 'items', 'keys', 'setdefault', 'update', 'values'] >>> import odbchelper >>> dir(odbchelper) (3) ['__builtins__', '__doc__', '__file__', '__name__', 'buildConnectionString'] (1) li est une liste, donc dir(li) retourne la liste de toutes les méthodes de liste. Notez que la liste retournée comprend les noms des méthodes sous forme de chaînes, pas les méthoses elles-mêmes. (2) d est un dictionnaire, donc dir(d) retourne la liste des noms de méthodes de dictionnaire. Au moins l’un de ces noms, keys, devrait être familier. (3) C’est ici que cela devient vraiment intéressant. odbchelper est un module, donc dir(odbchelper) retourne la liste de toutes les choses définies dans le module, y compris le attributs prédéfinis comme __name__ et __doc__ et tout attribut et méthode que vous définissez. Dans ce cas, odbchelper a une seule méthode définie par l’utilisateur, la fonction buildConnectionString que nous avons étudiée au Chapitre 2. Enfin, la fonction callable prend n’importe quel objet et retourne True si l’objet peut être appelé, sinon False. Les objets appelables sont les fonctions, les méthodes de classes ainsi que les classes elles-mêmes (nous verrons les classes au prochain chapitre). Exemple 4.8. Présentation de callable >>> import string >>> string.punctuation (1) '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' >>> string.join (2) >>> callable(string.punctuation) (3) False >>> callable(string.join) (4) True >>> print string.join.__doc__ (5) join(list [,sep]) -> string Return a string composed of the words in list, with intervening occurrences of sep. The default separator is a single space. (joinfields and join are synonymous) (1) Les fonctions du module string sont dépréciées (bien que beaucoup de gens utilisent encore la fonction join), mais le module comprend un grand nombre de constantes utiles comme ce string.punctuation, qui comprend tous les caractères de ponctuation standards. (2) string.join est une fonction qui effectue la jointure d’une liste de chaînes. (3) string.punctuation n’est pas appelable, c’est une chaîne. (Une chaîne a des méthodes appelables, mais elle n’est pas elle-même appelable.) (4) string.join est appelable, c’est une fonction qui prend deux arguments. (5) Tout objet appelable peut avoir une doc string. En utilisant la fonction callable sur chacun des attributs d’un objet, nous pouvons déterminer les attributs qui nous intéressent (méthodes, fonctions et classes) et ce que nous voulons ignorer (constantes etc.) sans savoir quoi que ce soit des objets à l’avance. 4.3.3. Fonctions prédéfinies type, str, dir et toutes les autres fonctions prédéfinies de Python sont regroupés dans un module spécial appelé __builtin__. (Il y a deux caractères de soulignement avant et deux après.) Pour vous aider, vous pouvez imaginer que Python exécute automatiquement from __builtin__ import * au démarrage, ce qui importe toutes les fonctions prédéfinies (built-in) dans l’espace de noms pour que vous puissiez les utiliser directement. L’avantage d’y penser de cette manière est que vous pouvez accéder à toutes les fonctions et attributs prédéfinis de manière groupée en obtenant des informations sur le module __builtin__. Et devinez quoi, nous avons une fonction pour ça, elle s’appelle info. Essayez vous-même et parcourez la liste maintenant, nous examinerons certaines des fonctions les plus importantes plus tard (certaines des classes d’erreur prédéfinies, comme AttributeError, devraient avoir l’air familier). Exemple 4.9. Attributs et fonctions prédéfinis >>> from apihelper import info >>> import __builtin__ >>> info(__builtin__, 20) ArithmeticError Base class for arithmetic errors. AssertionError Assertion failed. AttributeError Attribute not found. EOFError Read beyond end of file. EnvironmentError Base class for I/O related errors. Exception Common base class for all exceptions. FloatingPointError Floating point operation failed. IOError I/O operation failed. [...snip...] NOTE: Python est auto-documenté Python est fourni avec d’excellent manuels de référence que vous devriez parcourir de manière exhaustive pour apprendre tous les modules que Python offre. Mais alors que dans la plupart des langages vous auriez à vous référer constamment aux manuels (ou aux pages man, ou pire, à MSDN) pour vous rappeler l’usage de ces modules, Python est en grande partie auto-documenté. Pour en savoir plus sur les fonctions prédéfinies • La Python Library Reference (http://www.python.org/doc/current/lib/) documente toutes les fonctions prédéfinies (http://www.python.org/doc/ current/lib/built-in-funcs.html) et toutes les exceptions prédéfinies ( http://www.python.org/doc/current/lib/module-exceptions.html). 4.4. Obtenir des références objet avec getattr Vous savez déjà que les fonctions Python sont des objets. Ce que vous ne savez pas, c’est que vous pouvez obtenir une référence à une fonction sans connaître son nom avant l’exécution, à l’aide de la fonction getattr. Exemple 4.10. Présentation de getattr >>> li = ["Larry", "Curly"] >>> li.pop (1) >>> getattr(li, "pop") (2) >>> getattr(li, "append")("Moe") (3) >>> li ["Larry", "Curly", "Moe"] >>> getattr({}, "clear") (4) >>> getattr((), "pop") (5) Traceback (innermost last): File "", line 1, in ? AttributeError: 'tuple' object has no attribute 'pop' (1) Ceci retourne une référence à la méthode pop de la liste. Ce n’est pas un appel à la méthode pop, un appel se ferait par li.pop(). C’est la méthode elle-même. (2) Ceci retourne également une référence à la méthode pop, mais cette fois ci le nom de la méthode est passé comme argument de la fonction getattr. getattr est une fonction prédéfinie extrèmement utile qui retourne n’importe quel attribut de n’importe quel objet. Ici, l’objet est une liste et l’attribut est la méthode pop. (3) Au cas où vous ne voyez pas à quel point c’est utile, regardez ceci : la valeur de retour de getattr est la méthode, que vous pouvez alors appeler comme si vous aviez tapé li.append("Moe") directement. Mais vous n’avez pas appelé la fonction directement, vous avez passé le nom de la fonction comme paramètre sous forme de chaîne. (4) getattr fonctionne aussi avec les dictionnaires. (5) En théorie, getattr pourrait fonctionner avec les tuples, mais les tuples n’ont pas de méthodes et getattr déclenchera une exception quel que soit le nom d’attribut que vous lui donnez. 4.4.1. getattr et les modules getattr n’est pas seulement fait pour les types prédéfinis, il fonctionne aussi avec les modules. Exemple 4.11. getattr dans apihelper.py >>> import odbchelper >>> odbchelper.buildConnectionString (1) >>> getattr(odbchelper, "buildConnectionString") (2) >>> object = odbchelper >>> method = "buildConnectionString" >>> getattr(object, method) (3) >>> type(getattr(object, method)) (4) >>> import types >>> type(getattr(object, method)) == types.FunctionType True >>> callable(getattr(object, method)) (5) True (1) Ceci retourne une référence à la fonction buildConnectionString du module odbchelper, que nous avons étudié au Chapitre 2, Votre premier programme Python. (L’adresse hexadécimale qui s’affiche est spécifique à ma machine, votre sortie sera différente.) (2) A l’aide de getattr, nous pouvons obtenir la même référence à la même fonction. En général, getattr(objet, "attribut") est équivalent à objet.attribut. Si objet est un module, alors attribut peut être toute chose définie dans le module : une fonction, une classe ou une variable globale. (3) Voici ce que nous utilisons dans la fonction info. object est passé en argument à la fonction, method est une chaîne, le nom de la méthode ou de la fonction. (4) Dans ce cas, method est le nom d’une fonction, ce que nous prouvons en obtenant son type. (5) Puisque method est une fonction, elle est callable (appelable). 4.4.2. getattr comme sélecteur Une utilisation usuelle de getattr est dans le rôle de sélecteur. Par exemple, si vous avez un programme qui peut produire des données dans différents formats, vous pouvez définir des fonctions différentes pour chaque format de sortie et utiliser une fonction de sélection pour appeler celle qui convient. Par exemple, imaginons un programme qui affiche des statistiques de consultations d'un site Web aux formats HTML, XML et texte simple. Le choix du format de sortie peut être spécifié depuis la ligne de commande ou stocké dans un fichier de configuration. Un module statsout définit trois fonctions, output_html, output_xml et output_text. Ensuite, le programme principal définit une fonction de sortie unique, comme ceci : Exemple 4.12. Création d'un sélecteur avec getattr import statsout def output(data, format="text"): (1) output_function = getattr(statsout, "output_%s" % format) (2) return output_function(data) (3) (1) La fonction output prend un argument obligatoire, data et un argument optionnel, format. Si format n'est pas spécifié, il a la valeur par défaut text ce qui appelera la fonction de sortie en texte simple. (2) Nous concaténons l'argument format à "output_" pour produire un nom de fonction et allons chercher cette fonction dans le module statsout. Cela nous permet d'étendre simplement le programme plus tard pour supporter d'autres formats de sortie, sans changer la fonction de sélection. Il suffit d'ajouter une autre fonction à statsout nommée, par exemple, output_pdf et de passer "pdf" comme format à la fonction output. (3) Maintenant, nous pouvons simplement appeler la fonction de sortie comme toute autre fonction. La variable output_function est une référence à la fonction appropriée du module statsout. Avez vous vu le problème dans l'exemple précédent ? Il y a un couplage très lâche entre chaînes et fonctions et il n'y a aucune vérification d'erreur. Que se passe-t-il si l'utilisateur passe un format pour lequel aucune fonction correspondante n'est définie dans le module statsout? Et bien, getattr retournera None, qui sera assigné à output_function au lieu d'une fonction valide et à la ligne suivante, qui tente d'appeler cette fonction inexistante, plantera et déclenchera une exception. C'est un problème. Heureusement getattr prend un troisième argument optionnel, une valeurpar défaut. Exemple 4.13. Valeurs par défaut de getattr import statsout def output(data, format="text"): output_function = getattr(statsout, "output_%s" % format, statsout.output_text) return output_function(data) (1) (1) Cet appel de fonction est assuré de fonctionner puisque nous avons ajouté un troisième argument à l'appel à getattr. Le troisième argument est une valeur par défaut qui est retournée si l'attribut ou la méthode spécifié par le second argument n'est pas trouvé. Comme vous pouvez le voir, getattr est très puissant. C'est le coeur même de l'introspection et vous en verrez des exemples encore plus puissants dans des prochains chapitres. 4.5. Filtrage de listes Comme vous le savez, Python a des moyens puissant de mutation d’une liste en une autre, au moyen des list comprehensions (Section 3.6, «Mutation de listes» ). Cela peut être associé à un mécanisme de filtrage par lequel certains éléments sont modifiés alors que d’autres sont totalement ignorés. Voici la syntaxe du filtrage de liste : [mapping-expression for element in source-list if filter-expression] C’est une extension des list comprehensions que vous connaissez et appréciez. Les deux premiers tiers sont identiques, la dernière partie, commençant par le if, est l’expression de filtrage. Une expression de filtrage peut être n’importe quelle expression qui s’évalue en vrai ou faux (ce qui en Python peut être presque tout). Tout élément pour lequel l’expression de filtrage s’évalue à vrai sera inclu dans la liste à transformer. Tous les autres éléments seront ignorés, il ne passeront jamais par l’expression de mutation et ne seront pas inclus dans la liste retournée. Exemple 4.14. Présentation du filtrage de liste >>> li = ["a", "mpilgrim", "foo", "b", "c", "b", "d", "d"] >>> [elem for elem in li if len(elem) > 1] (1) ['mpilgrim', 'foo'] >>> [elem for elem in li if elem != "b"] (2) ['a', 'mpilgrim', 'foo', 'c', 'd', 'd'] >>> [elem for elem in li if li.count(elem) == 1] (3) ['a', 'mpilgrim', 'foo', 'c'] (1) L’expression de mutation est ici très simple (elle retourne juste la valeur de chaque élément), observez plutôt attentivement l’expression de filtrage. Au fur et à mesure que Python parcours la liste, il soumet chaque élément à l’expression de filtrage, si l’expression s’évalue à vrai, l’élément passe par l’expression de mutation et le résultat est inclu dans la liste de résultat. Ici, on filtre toutes les chaînes d’un seul caractère, il ne reste donc que les chaînes plus longues. (2) Ici, on filtre une valeur spécifique : b. Notez que cela filtre toutes les occurences de b, puisqu’à chaque fois qu’il apparaît, l’expression de filtrage s’évaluera à faux. (3) count est une méthode de listes qui retourne le nombre d’occurences d’une valeur dans la liste. On pourrait penser que ce filtre élimine les doublons de la liste, retournant une liste contenant seulement un exemplaire de chaque valeur. Mais en fait, les valeurs qui apparaissent deux fois dans la liste initiale (ici b et d) sont totalement éliminées. Il y a des moyens de supprimer les doublons d’une liste mais le filtrage n’est pas la solution. Revenons à cette ligne de apihelper.py: methodList = [method for method in dir(object) if callable(getattr(object, method))] Cela à l’air complexe et ça l’est, mais la structure de base est la même. L’expression complète renvoie une liste qui est assignée à la variable methodList. La première moitié de l’expression est la mutation de liste. L’expression de mutation est une expression d’identité, elle retourne la valeur de chaque élément. dir(object) retourne une liste des attributs et méthodes de object, c’est à cette liste que vous appliquez la mutation. La seule nouveauté est l’expression après le if. L’expression de filtrage à l’air impressionant, mais elle n’est pas si terrible. Vous connaissez déjà callable, getattr et in. Comme vous l’avez vu dans la section précédente, l’expression getattr(object, method) retourne un objet fonction si object est un module et si method est le nom d’une fonction de ce module. Donc, cette expression prend un objet, appelé object, obtient une liste des noms de ses attributs, méthodes, fonctions et quelques autres choses, puis filtre cette liste pour éliminer ce qui ne nous intéresse pas. Cette élimination se fait en prenant le nom de chaque attribut/méthode/fonction et en obtenant une référence vers l’objet véritable, grâce à la fonction getattr. On vérifie alors si cet objet est appelable, ce qui sera le cas pour toutes les méthodes et fonctions prédéfinies (comme la méthode pop d’une liste) ou définies par l’utilisateur ( comme la fonction buildConnectionString du module odbchelper). Nous ne nous intéressons pas aux autres attributs, comme l’attribut __name__ qui existe pour tout module. Pour en savoir plus sur le filtrage de liste • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite d’une autre manière de filtrer les listes en utilisant la fonction prédéfinie filter (http://www.python.org/doc/current/tut/node7.html# SECTION007130000000000000000). 4.6. Particularités de and et or En Python, and et or appliquent la logique booléenne comme vous pourriez l’attendre, mais ils ne retournent pas de valeurs booléennes, ils retournent une des valeurs comparées. Exemple 4.15. Présentation de and >>> 'a' and 'b' (1) 'b' >>> '' and 'b' (2) '' >>> 'a' and 'b' and 'c' (3) 'c' (1) Lorsqu’on utilise and les valeurs sont évaluées dans un contexte booléen de gauche à droite. 0, '', [], (), {} et None valent faux dans ce contexte, tout le reste vaut vrai.^[1]Si toutes les valeurs valent vrai dans un contexte booléen, and retourne la dernière valeur. Ici and évalue 'a', qui vaut vrai, puis 'b', qui vaut vrai et retourne 'b'. (2) Si une des valeurs vaut faux and retourne la première valeur fausse. Ici '' est la première valeur fausse. (3) Toutes les valeurs sont vrai, donc and retourne la dernière valeur, 'c'. Exemple 4.16. Présentation de or >>> 'a' or 'b' (1) 'a' >>> '' or 'b' (2) 'b' >>> '' or [] or {} (3) {} >>> def sidefx(): ... print "in sidefx()" ... return 1 >>> 'a' or sidefx() (4) 'a' (1) Lorsqu’on utilise or, les valeurs sont évaluées dans un contexte booléen de gauche à droite, comme pour and. Si une des valeurs vaut vrai, or la retourne immédiatement. Dans ce cas 'a' est la première valeur vraie. (2) or évalue '', qui vaut faux, puis 'b', qui vaut vrai et retourne 'b'. (3) Si toutes les valeurs valent faux, or retourne la dernière valeur. or évalue '', qui vaut faux, puis [], qui vaut faux, puis {}, qui vaut faux et retourne {}. (4) Notez que or continue l’évaluation seulement jusqu’à ce qu’il trouve une valeur vraie, le reste est ignoré. C’est important si certaines valeurs peuvent avoir un effet de bord. Ici, la fonction sidefx n’est jamais appelée, car or évalue 'a', qui vaut vrai et retourne 'a' immédiatement. Si vous êtes un programmeur C, vous êtes certainement familier de l’expression ternaire bool ? a : b, qui s’évalue à a si bool vaut vrai et à b dans le cas contraire. Le fonctionnement de and et or en Python vous permet d’accomplir la même chose. 4.6.1. Utilisation de l'astuce and-or Exemple 4.17. Présentation de l’astuce and-or >>> a = "first" >>> b = "second" >>> 1 and a or b (1) 'first' >>> 0 and a or b (2) 'second' (1) Cette syntaxe ressemble à celle de l’expression ternaire bool ? a : b de C. L’expression est évaluée de gauche à droite, donc le and est évalué en premier. 1 and 'first' s’évalue à 'first', puis 'first' or 'second' s’évalue à 'first'. (2) 0 and 'first' s’évalue à 0, puis 0 or 'second' s’évalue à 'second'. Cependant, puisque cette expression en Python est simplement de la logique booléenne et non un dispositif spécial du langage, il y a une différence très, très importante entre l’astuce and-or en Python et la syntaxe bool ? a : b en C. Si a vaut faux, l’expression ne fonctionnera pas comme vous vous y attendez. (Vous devinez que cela m’a déjà joué des tours. Et plus d’une fois !) Exemple 4.18. Quand l’astuce and-or échoue >>> a = "" >>> b = "second" >>> 1 and a or b (1) 'second' (1) Puisque a est une chaîne vide, ce que Python évalue à faux dans un contexte booléen, 1 and '' s’évalue à '', puis '' or 'second' s’évalue à 'second'. Zut ! Ce n’est pas ce que nous voulions. L’astuce and-or, bool and a or b, ne fonctionne pas comme l’expression ternaire de C bool ? a : b quand a s’évalue à faux dans un contexte booléen. La véritable astuce cachée derrière l’astuce and-or, c’est de s’assurer que a ne vaut jamais faux. Une manière habituelle de le faire est de changer a en [a] et b en [b] et de prendre le premier élément de la liste retournée, qui sera soit a soit b. Exemple 4.19. L’astuce and-or en toute sécurité >>> a = "" >>> b = "second" >>> (1 and [a] or [b])[0] (1) '' (1) Puisque [a] est une liste non-vide, il ne vaut jamais faux. Même si a est 0 ou '' ou une autre valeur fausse, la liste [a] vaut vrai puisqu’elle a un élément. On peut penser que cette astuce apporte plus de complication que d’avantages. Après tout, on peut obtenir le même résultat avec une instruction if, alors pourquoi s’embarasser de tout ces problèmes ? Mais dans de nombreux cas, le choix se fait entre deux valeurs constantes et donc vous pouvez utiliser cette syntaxe plus simple sans vous inquiéter puisque vous savez que a vaudra toujours vrai. Et même si vous devez utiliser la version sûre plus complexe, il y a parfois de bonnes raisons de le faire, il y a des cas en Python où les instructions if ne sont pas autorisées, comme dans les fonctions lambda. Pour en savoir plus sur l'astuce and-or • Le Python Cookbook (http://www.activestate.com/ASPN/Python/Cookbook/) traite des alternatives à l’astuce and-or (http://www.activestate.com/ASPN/ Python/Cookbook/Recipe/52310). 4.7. Utiliser des fonctions lambda Python permet une syntaxe intéressante qui vous laisse définir des mini-fonctions d’une ligne à la volée. Empruntées à Lisp, ces fonctions dites lambda peuvent être employées partout où une fonction est nécéssaire. Exemple 4.20. Présentation des fonctions lambda >>> def f(x): ... return x*2 ... >>> f(3) 6 >>> g = lambda x: x*2 (1) >>> g(3) 6 >>> (lambda x: x*2)(3) (2) 6 (1) Voici une fonction lambda qui fait la même chose que la fonction ordinaire précédente. Notez la syntaxe condensée : il n’y a pas de parenthèses autour de la liste d’arguments et le mot-clé return est manquant (il est implicite, la fonction complète ne pouvant être qu’une seule expression). Remarquez aussi que la fonction n’a pas de nom, mais qu’elle peut être appelée à travers la variable à laquelle elle est assignée. (2) Vous pouvez utiliser une fonction lambda sans l’assigner à une variable. Ce n’est pas forcément très utile, mais cela démontre qu’une fonction lambda est simplement une fonction en ligne. Plus généralement, une fonction lambda est une fonction qui prend un nombre quelconque d’arguments (y compris des arguments optionnels) et retourne la valeur d’une expression unique. Les fonctions lambda ne peuvent pas contenir de commandes et elles ne peuvent contenir plus d’une expression. N’essayez pas de mettre trop de choses dans une fonction lambda, si vous avez besoin de quelque chose de complexe, définissez plutôt une fonction normale et faites la aussi longue que vous voulez. NOTE: lambda est optionnel Les fonctions lambda sont une question de style. Les utiliser n’est jamais une nécessité, partout où vous pouvez les utiliser, vous pouvez utiliser une fonction ordinaire. Je les utilise là où je veux incorporer du code spécifique et non réutilisable sans encombrer mon code de multiples fonctions d’une seule ligne. 4.7.1. Les fonctions lambda dans le monde réel Voici les fonctions lambda dans apihelper.py: processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s) Il y a plusieurs chose à noter ici. D’abord, nous utilisons la forme simple de l’astuce and-or, ce qui est sûr car une fonction lambda vaut toujours vrai dans un contexte booléen. (Cela ne veut pas dire qu’une fonction lambda ne peut retourner faux. La fonction est toujours vrai, sa valeur de retour peut être vrai ou fausse.) Ensuite, nous utilisons la fonction split sans arguments. Vous l’avez déjà vu employée avec un ou deux arguments, sans arguments elle utilise les espaces comme séparateur. Exemple 4.21. split sans arguments >>> s = "this is\na\ttest" (1) >>> print s this is a test >>> print s.split() (2) ['this', 'is', 'a', 'test'] >>> print " ".join(s.split()) (3) 'this is a test' (1) Voici une chaîne multi-lignes définie à l’aide de caractères d’échappement au lieu de triples guillemets. \n est le retour chariot et \t le caractère de tabulation. (2) split sans arguments fait la séparation sur les espaces. Trois espaces, un retour chariot ou un caractère de tabulation reviennent à la même chose. (3) Vous pouvez normaliser les espaces en utilisant split sur une chaîne et en la joignant par join avec un espace simple comme délimiteur. C’est ce que fait la fonction info pour replier les doc string sur une seule ligne. Mais que fait donc exactement cette fonction info avec ces fonctions lambda, ces split et ces astuces and-or ? processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s) processFunc est maintenant une fonction, mais la fonction qu’elle est dépend de la valeur de la variable collapse. Si collapse vaut vrai, processFunc(string) repliera les espaces, sinon, processFunc(string) retournera son argument sans le modifier. Pour faire la même chose dans un langage moins robuste, tel que Visual Basic, vous auriez sans doute créé une fonction prenant une chaîne et un argument collapse qui aurait utilisé une instruction if pour décider de replier les espaces ou non, puis aurait retourné la valeur appropriée. Cela serait inefficace car la fonction devrait prendre en compte tous les cas possibles. A chaque fois que vous l’appeleriez, elle devrait décider si elle doit replier l’espace avant de pouvoir vous donner ce que vous souhaitez. En Python, vous pouvez retirer cette prise de décision de la fonction et définir une fonction lambda taillée sur mesure pour vous donner ce que vous voulez et seulement cela. C’est plus efficace, plus élégant et moins sujet à des erreurs dans l’ordre des arguments. Pour en savoir plus sur les fonctions lambda • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) traite de l’utilisation de lambda pour faire des appels de fonction indirects (http://www.faqts.com/knowledge-base/view.phtml/aid/6081 /fid/241). • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) montre comment accéder à des variables extérieures de l’intérieur d’une fonction lambda (http://www.python.org/doc/current/tut/node6.html# SECTION006740000000000000000). (La PEP 227 (http://python.sourceforge.net/ peps/pep-0227.html) explique comment cela va changer dans les futures versions de Python.) • La The Whole Python FAQ (http://www.python.org/doc/FAQ.html) a des exemples de code obscur monoligne utilisant lambda (http://www.python.org/cgi-bin/ faqw.py?query=4.15&querytype=simple&casefold=yes&req=search). 4.8. Assembler les pièces La dernière ligne du code, la seule que nous n’ayons pas encore déconstruite, est celle qui fait tout le travail. Mais arrivé à ce point, le travail est simple puisque tous les éléments dont nous avons besoin sont disponibles. Les dominos sont en place, il ne reste qu’à les faire tomber. Voici le plat de résistance de apihelper.py: print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList]) Notez que ce n’est qu’une commande, répartie sur plusieurs lignes sans utiliser le caractère de continuation (<<\>>). Vous vous rappelez quand j’ai dit que certaines expressions peuvent être divisées en plusieurs lignes sans utiliser de backslash ? Une list comprehension est une expression de ce type car toute l’expression est entourée de crochets. Maintenant étudions l’expression de la fin vers le début. L'instruction for method in methodList nous montre qu’il s’agit d’une list comprehension. Comme vous le savez, methodList est une liste de toutes les méthodes qui nous intéressent dans object. Nous parcourons donc cette liste avec method. Exemple 4.22. Obtenir une doc string dynamiquement >>> import odbchelper >>> object = odbchelper (1) >>> method = 'buildConnectionString' (2) >>> getattr(object, method) (3) >>> print getattr(object, method).__doc__ (4) Build a connection string from a dictionary of parameters. Returns string. (1) Dans la fonction info, object est l’objet pour lequel nous demandons de l’aide, passé en argument. (2) Pendant que nous parcourons la methodList, method est le nom de la méthode en cours. (3) En utlisant la fonction getattr, nous obtenons une référence à la fonction method du module object. (4) Maintenant, afficher la doc string de la méthode est facile. La pièce suivante du puzzle est l’utilisation de str sur la doc string. Comme vous vous rappelez peut-être, str est une fonction prédéfinie pour convertir des données en chaîne. Mais une doc string est toujours une chaîne, alors pourquoi utiliser str ? La réponse est que toutes les fonctions n’ont pas de doc string, et que l’attribut __doc__ de celles qui n’en ont pas renvoi None. Exemple 4.23. Pourquoi utiliser str sur une doc string ? >>> >>> def foo(): print 2 >>> >>> foo() 2 >>> >>> foo.__doc__ (1) >>> foo.__doc__ == None (2) True >>> str(foo.__doc__) (3) 'None' (1) Nous pouvons facilement définir une fonction qui n’a pas de doc string, sont attribut __doc__ est None. Attention, si vous évaluez directement l’attribut __doc__, l'IDE Python n’affiche rien du tout, ce qui est logique si vous y réflechissez mais n’est pas très utile. (2) Vous pouvez vérifier que la valeur de l’attribut __doc__ est bien None en faisant directement la comparaison. (3) Lorsque l’on utilise la fonction str, elle prend la valeur nulle et en retourne une représentation en chaîne, 'None'. NOTE: Python vs. SQL : comparaison des valeurs nulles En SQL, vous devez utiliser IS NULLmethod au lieu de = NULL pour la comparaison d’une valeur nulle. En Python, vous pouvez utiliser aussi bien == None que is None, mais is None est plus rapide. Maintenant que nous sommes sûrs d’obtenir une chaîne, nous pouvons passer la chaîne à processFunc, que nous avons déjà défini comme une fonction qui replie ou non les espace. Maintenant vous voyez qu’il était important d’utiliser str pour convertir une valeur None en une représentation en chaîne. processFunc attend une chaîne comme argument et appelle sa méthode split, ce qui échouerait si nous passions None, car None n’a pas de méthode split. En remontant en arrière encore plus loin, nous voyons que nous utilisons encore le formatage de chaîne pour concaténer la valeur de retour de processFunc avec celle de la méthode ljust de method. C’est une nouvelle méthode de chaîne que nous n’avons pas encore rencontré. Exemple 4.24. Présentation de la méthode ljust >>> s = 'buildConnectionString' >>> s.ljust(30) (1) 'buildConnectionString ' >>> s.ljust(20) (2) 'buildConnectionString' (1) ljust complète la chaîne avec des espaces jusqu’à la longueur donnée. La fonction info l’utilise pour afficher sur deux colonnes et aligner les doc string de la seconde colonne. (2) Si la longueur donnée est plus petite que la longueur de la chaîne, ljust retourne simplement la chaîne sans la changer. Elle ne tronque jamais la chaîne. Nous avons presque terminé. Ayant obtenu le nom de méthode complété d’espaces de la méthode ljust et la doc string (éventuellement repliée sur une ligne) de l’appel à processFunc, nous concaténons les deux pour obtenir une seule chaîne. Comme nous faisons une mutation de methodList, nous obtenons une liste de chaînes. En utlisant la méthode join de la chaîne "\n", nous joignons cette liste en une chaîne unique, avec chaque élément sur une ligne et affichons le résultat.. Exemple 4.25. Affichage d’une liste >>> li = ['a', 'b', 'c'] >>> print "\n".join(li) (1) a b c (1) C’est aussi une astuce de débogage utile lorsque vous travaillez avec des liste. Et en Python, vous travaillez toujours avec des listes. C’est la dernière pièce du puzzle. Le code devrait maintenant être parfaitement compréhensible. print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList]) 4.9. Résumé Le programme apihelper.py et sa sortie devraient maintenant être parfaitement clairs. def info(object, spacing=10, collapse=1): """Print methods and doc strings. Takes module, class, list, dictionary, or string.""" methodList = [method for method in dir(object) if callable(getattr(object, method))] processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s) print "\n".join(["%s %s" % (method.ljust(spacing), processFunc(str(getattr(object, method).__doc__))) for method in methodList]) if __name__ == "__main__": print info.__doc__ Voici la sortie de apihelper.py : >>> from apihelper import info >>> li = [] >>> info(li) append L.append(object) -- append object to end count L.count(value) -> integer -- return number of occurrences of value extend L.extend(list) -- extend list by appending list elements index L.index(value) -> integer -- return index of first occurrence of value insert L.insert(index, object) -- insert object before index pop L.pop([index]) -> item -- remove and return item at index (default last) remove L.remove(value) -- remove first occurrence of value reverse L.reverse() -- reverse *IN PLACE* sort L.sort([cmpfunc]) -- sort *IN PLACE*; if given, cmpfunc(x, y) -> -1, 0, 1 Avant de plonger dans le chapitre suivant, assurez vous que vous vous sentez à l’aise pour : • Définir et appeler des fonctions avec des arguments optionnels et nommés • Utiliser str pour convertir une valeur quelconque en chaîne • Utiliser getattr pour obtenir des références à des fonctions et autres attributs dynamiquement • Etendre la syntaxe des list comprehensions pour faire du filtrage de liste • Identifier l’astuce and-or et l’utiliser de manière sûre • Définir des fonctions lambda • Assigner des fonctions à des variables et appeler ces fonctions en référençant les variables. Je n’insisterais jamais assez : cette manière de penser est essentielle pour faire progresser votre compréhension de Python. Vous verrez des applications plus complexe de ce concept tout au long de ce livre. ━━━━━━━━━━━━━━ ^[1] Presque tout, en fait. Par défaut, les instances de classes valent vrai dans un contexte booléen mais vous pouvez définir des méthodes spéciales de votre classe pour faire qu’une instance vale faux. Vous apprendrez tout sur les classes et les méthodes spéciales au Chapitre 5. Chapitre 5. Les objets et l'orienté objet Ce chapitre, comme la plupart de ceux qui le suivent, a trait à la programmation orientée objet en Python. 5.1. Plonger Voici un programme Python complet et fonctionnel. Lisez les doc string du module, des classes et des fonctions pour avoir un aperçu de ce que ce programme fait et de son fonctionnement. Comme d'habitude ne vous inquiétez pas de ce que vous ne comprenez pas, la suite du chapitre est là pour vous l'expliquer. Exemple 5.1. fileinfo.py Si vous ne l’avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples (http://diveintopython.org/download/ diveintopython-examples-5.4.zip) du livre. """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"]): (1) print "\n".join(["%s=%s" % (k, v) for k, v in info.items()]) print (1) La sortie de ce programme dépend des fichiers qui se trouvent sur votre disque dur. Pour avoir une sortie pertinente, vous devrez changer le chemin pour qu'il pointe vers un répertoire de fichiers MP3 sur votre machine. Voici la sortie que j'ai obtenu sur ma machine. Votre sortie sera différente, sauf si, par une surprenante coïncidence, vous partagez exactement mes goûts musicaux.. album= artist=Ghost in the Machine title=A Time Long Forgotten (Concept genre=31 name=/music/_singles/a_time_long_forgotten_con.mp3 year=1999 comment=http://mp3.com/ghostmachine album=Rave Mix artist=***DJ MARY-JANE*** title=HELLRAISER****Trance from Hell genre=31 name=/music/_singles/hellraiser.mp3 year=2000 comment=http://mp3.com/DJMARYJANE album=Rave Mix artist=***DJ MARY-JANE*** title=KAIRO****THE BEST GOA genre=31 name=/music/_singles/kairo.mp3 year=2000 comment=http://mp3.com/DJMARYJANE album=Journeys artist=Masters of Balance title=Long Way Home genre=31 name=/music/_singles/long_way_home1.mp3 year=2000 comment=http://mp3.com/MastersofBalan album= artist=The Cynic Project title=Sidewinder genre=18 name=/music/_singles/sidewinder.mp3 year=2000 comment=http://mp3.com/cynicproject album=Digitosis@128k artist=VXpanded title=Spinning genre=255 name=/music/_singles/spinning.mp3 year=2000 comment=http://mp3.com/artists/95/vxp 5.2. Importation de modules avec from module import Python fournit deux manières d'importer les modules. Les deux sont utiles et vous devez savoir quand les utiliser. Vous avez déjà vu la première, import module, à la Section 2.4, «Tout est objet». La deuxième manière accomplit la même action avec des différences subtiles mais importante dans son fonctionnement. Voici la syntaxe de base de from module import : from UserDict import UserDict Cela ressemble à la syntaxe import module que vous connaissez, mais avec une différence importante : les attributs et et les méthodes du module importé types sont importés directement dans l'espace de noms local, ils sont donc disponible directement sans devoir les qualifier avec le nom du module. Vous pouvez importer des éléments précis ou utiliser from module import * pour tout importer. NOTE: Python vs. Perl : from module import from module import * en Python est comme use module in Perl. import module en Python est comme require module en Perl. NOTE: Python vs. Java : from module import from module import * en Python est comme import module.* en Java. import module en Python est comme import module en Java. Exemple 5.2. import module vs. from module import >>> import types >>> types.FunctionType (1) >>> FunctionType (2) Traceback (innermost last): File "", line 1, in ? NameError: There is no variable named 'FunctionType' >>> from types import FunctionType (3) >>> FunctionType (4) (1) Le module types ne contient aucune méthode, seulement des attributs pour chaque type d'objet Python. Notez que l'attribut FunctionType doit être qualifié avec le nom de module, types. (2) FunctionType lui-même n'a pas été défini dans cet espace de noms, il n'existe que dans le contexte de types. (3) Cette syntaxe permet l'importation de l'attribut FunctionType du module types directement dans l'espace de noms local. (4) Maintenant FunctionType peut être référé directement, sans mentionner types. Quand faut-il utiliser from module import ? • Quand vous devez accéder fréquemment aux attributs et méthodes et que vous ne voulez pas taper le nom de module sans arrêt, utilisez from module import. • Quand vous voulez n'importer que certains attributs et méthodes, utilisez from module import. • Quand le module contient des attributs ou des fonctions ayant des noms déjà utilisés dans votre module, vous devez utiliser import module pour éviter les conflits de nom. En dehors de ces cas, c'est une question de style et vous verrez du code Python écrit des deux manières. Attention: Utilisez from module import * avec modération, il rend plus difficile de déterminer l'origine d'une fonction ou d'un attribut, ce qui rend le débogage et la refactorisation plus difficiles. Pour en savoir plus sur l'importation de module • eff-bot (http://www.effbot.org/guides/) a d'autres choses à ajouter à propos de import module et from module import (http://www.effbot.org/guides /import-confusion.htm). • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite de techniques d'import avancées, y compris from module import * (http:// www.python.org/doc/current/tut/node8.html#SECTION008410000000000000000). 5.3. Définition de classes Python est entièrement orienté objet : vous pouvez définir vos propres classes, hériter de vos classes ou des classes prédéfinies et instancier les classes que vous avez défini. Définir une classe en Python est simple, comme pour les fonctions, il n'y a pas de définition séparée d'interface. Vous définissez simplement la classe et commencez à coder. Une classe Python commence par le mot réservé class suivi du nom de la classe. Techiquement c'est tout ce qui est requis, une classe n'hérite pas obligatoirement d'une autre. Exemple 5.3. La classe Python la plus simple class Loaf: (1) pass (2) (3) (1) Le nom de cette classe est Loaf et elle n'hérite d'aucune autre classe. Chaque mot d'un nom de classe prend habituellement une majuscule, DeCetteManiere, mais c'est une simple convention et pas une nécéssité. (2) Cette classe ne définit aucune méthode ni attribut, mais pour respecter la syntaxe, il est nécéssaire d'avoir quelque chose dans la définition, nous utilisons donc pass. C'est un mot réservé de Python qui signifie simplement <>. C'est une instruction qui ne fait rien et c'est un bon marqueur lorsque vous écrivez un squelette de fonction ou de classe. (3) Vous l'aurez sans doute deviné, tout est indenté dans une classe, comme le code d'une fonction, d'une instruction if, d'une boucle for etc. La première ligne non indenté ne fait plus partie de la classe. NOTE: Python vs. Java : pass L'instruction pass de Python est comme une paire d'accolades vide ({}) en Java ou C. Bien sûr, dans des cas réels la plupart des classes hériteront d'autres classes et elles définiront leurs propres classes, méthodes et attributs. Mais comme vous l'avez vu, il n'y a rien qu'une classe doit absolument avoir en dehors d'un nom. En particulier, les programmeurs C++ s'étonneront sans doute que les classes Python n'aient pas de constructeurs et de destructeurs explicites. Les classes Python ont quelque chose de semblable à un constructeur : la méthode __init__. Exemple 5.4. Définition de la classe FileInfo from UserDict import UserDict class FileInfo(UserDict): (1) (1) En Python, l'ancêtre d'une classe est simplement indiqué entre parenthèses immédiatement après le nom de la classe. La classe FileInfo est hérite donc de la classe UserDict (qui a été importée du module UserDict). UserDict est une classe qui se comporte comme un dictionnaire, vous permettant pratiquement de dériver le type de données dictionnaire et d'y ajouter votre propre comportement (il y a des classes semblables UserList et UserString qui vous permettent de dériver les listes et les chaînes). Il y a un peu de magie noire derrière tout cela, nous la démystifierons plus loin dans ce chapitre lorsque nous explorerons la classe UserDict plus en détail. NOTE: Python vs. Java : ancêtres En Python, l'ancêtre d'une classe est simplement indiqué entre parenthèses immédiatement après le nom de la classe. Il n'y a pas de mot clé spécifique comme extends en Java. Python supporte l'héritage multiple. Entre les parenthèses qui suivent le nom de classe, vous pouvez indiquer autant de classes ancêtres que vous le souhaitez, séparées par des virgules. 5.3.1. Initialisation et écriture de classes Cet exemple montre l'initialisation de la classe FileInfo avec la méthode __init__. Exemple 5.5. Initialisation de la classe FileInfo class FileInfo(UserDict): "store file metadata" (1) def __init__(self, filename=None): (2) (3) (4) (1) Les classes peuvent aussi (et le devraient) avoir une doc string, comme les modules et les fonctions. (2) __init__ est appelé immédiatement après qu'une instance de la classe est créée. Il serait tentant mais incorrect de l'appeler le constructeur de la classe. Tentant, parceque ça ressemble à un constructeur (par convention, __init__ est la première méthode définie de la classe), ça se comporte comme un constructeur (c'est le premier morceau de code exécuté dans une nouvelle instance de la classe) et que ça sonne pareil (<> fait penser à quelque chose comme un constructeur). Incorrect, parce qu'au moment ou __init__ est appelé, l'objet à déjà été créé et qu vous avez déjà une référence valide à la nouvelle instance de la classe. Mais __init__ est ce qui se rapproche le plus d'un constructeur en Python et remplit en gros le même rôle. (3) Le premier argument de chaque méthode de classe, y compris __init__, est toujours une référence à l'instance actuelle de la classe. Par convention, cet argument est toujours nommé self. Dans la méthode __init__, self fait référence à l'objet nouvellement créé, dans les autres méthodes de classe, il fait référence à l'instance dont la méthode a été appelée. Bien que vous deviez spécifier self explicitement lorsque vous définissez la méthode, vous ne devez pas le spécifier lorsque vous appelez la méthode, Python l'ajoutera pour vous automatiquement. (4) Une méthode __init__ peut prendre n'importe quel nombre d'arguments et tout comme pour les fonctions, les arguments peuvent être définis avec des valeurs par défaut, ce qui les rend optionnels lors de l'appel. Ici filename a une valeur par défaut de None, la valeur nulle de Python. NOTE: Python vs. Java : self Par convention, le premier argument d'une méthode de classe (la référence à l'instance en cours) est appelé self. Cet argument remplit le rôle du mot réservé this en C++ ou Java, mais self n'est pas un mot réservé de Python, seulement une convention de nommage. Cependant, veuillez ne pas l'appeler autre chose que self, c'est une très forte convention. Exemple 5.6. Ecriture de la classe FileInfo class FileInfo(UserDict): "store file metadata" def __init__(self, filename=None): UserDict.__init__(self) (1) self["name"] = filename (2) (3) (1) Certain langage pseudo-orientés objet comme Powerbuilder ont un concept d' <> des constructeurs et autres évènements, dans lequel la méthode de l'ancêtre est appelée automatiquement avant que la méthode du descendant soit exécutée. Python n'a pas ce comportement, vous devez appeler la méthode appropriée de l'ancêtre explicitement. (2) Je vous ai dit que cette classe se comportait comme un dictionnaire, en voici le premier signe. Nous assignons l'argument filename comme valeur de la clé name de cet objet. (3) Notez que la méthode __init__ ne retourne jamais de valeur. 5.3.2. Quand utiliser self et __init__ Lorsque vous définissez vos méthodes de classe, vous devez indiquer explicitement self comme premier argument de chaque méthode, y compris __init__. Quand vous appelez une méthode d'une classe ancêtre depuis votre classe, vous devez inclure l'argument self. Mais quand vous appelez votre méthode de classe de l'extérieur, vous ne spécifiez rien pour l'argument self, vous l'omettez complètement et Python ajoute automatiquement la référence d'instance. Je me rends bien compte qu'on s'y perd au début, ce n'est pas réellement incohérent même si cela peut sembler l'être car cela est basé sur une distinction (entre méthode liée et non liée) que vous ne connaissez pas pour l'instant. Ouf. Je sais bien que ça fait beaucoup à absorber, mais vous ne tarderez pas à comprendre tout ça. Toutes les classes Python fonctionnent de la même manière, donc quand vous en avez appris une, vous les connaissez toutes. Mais même si vous oubliez tout le reste souvenez vous de ça, car ça vous jouera des tours : NOTE: Méthodes __init__ Les méthodes __init__ sont optionnelles, mais quand vous en définissez une, vous devez vous rappeler d'appeler explicitement la méthode __init__ de l'ancêtre de la classe. C'est une règle plus générale : quand un descendant veut étendre le comportement d'un ancêtre, la méthode du descendant doit appeler la méthode de l'ancêtre explicitement au moment approprié, avec les arguments appropriés. Pour en savoir plus sur les classes Python • Learning to Program (http://www.freenetpages.co.uk/hp/alan.gauld/) a une introduction en douceur aux classes (http://www.freenetpages.co.uk/hp/ alan.gauld/tutclass.htm). • How to Think Like a Computer Scientist (http://www.ibiblio.org/obp/ thinkCSpy/) montre comment utiliser des classes pour modéliser des types composés (http://www.ibiblio.org/obp/thinkCSpy/chap12.htm). • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) a un aperçu en profondeur des classes, des espaces de noms et de l'héritage ( http://www.python.org/doc/current/tut/node11.html). • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) répond à des questions courantes à propos des classes (http:// www.faqts.com/knowledge-base/index.phtml/fid/242). 5.4. Instantiation de classes L'instanciation de classes en Python est simple et directe. Pour instancier une classe, appelez simplement la classe comme si elle était une fonction, en lui passant les arguments que la méthode __init__ définit. La valeur de retour sera l'objet nouvellement créé. Exemple 5.7. Création d'une instance de FileInfo >>> import fileinfo >>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3") (1) >>> f.__class__ (2) >>> f.__doc__ (3) 'store file metadata' >>> f (4) {'name': '/music/_singles/kairo.mp3'} (1) Nous créons une instance de la classe FileInfo (définie dans le module fileinfo) et assignons l'instance nouvellement créée à la variable f. Nous passons un paramètre, /music/_singles/kairo.mp3, qui sera l'argument filename de la méthode __init__ de FileInfo. (2) Chaque instance de classe à un attribut prédéfini, __class__, qui est la classe de l'objet (notez que la représentation de cet attribut comprend l'adresse physique de l'instance sur ma machine, votre sortie sera différente). Les programmeurs Java sont sans doute familiers de la classe Class, qui contient des méthodes comme getName et getSuperclass permettant d'obtenir les métadonnées d'un objet. En Python, ce type de métadonnées est accessible directement par le biais de l'objet lui-même à travers des attributs comme __class__, __name__ et __bases__. (3) Vous pouvez accéder à la doc string de l'instance comme pour une fonction ou un module. Toutes les instances d'une classe partagent la même doc string. (4) Rappelez vous quand la méthode __init__ a assigné son argument filename à self["name"]. Voici le résultat. Les arguments que nous passons lorsque nous créons une instance de classe sont envoyés directement à la méthode __init__ (en même temps que la référence à l'objet, self, que Python ajoute automatiquement). NOTE: Python vs. Java : instantiation de classes En Python, vous appelez simplement une classe comme si c'était une fonction pour créer une nouvelle instance de la classe. Il n'y a pas d'opérateur new explicite comme pour C++ ou Java. 5.4.1. Ramasse-miettes Si créer des instances est simple, les détruire est encore plus simple. En général, il n'y a pas besoin de libérer explicitement les instances, elles sont libérées automatiquement lorsque les variables auxquelles elles sont assignées sont hors de portée. Les fuites mémoire sont rares en Python. Exemple 5.8. Tentative d'implémentation d'une fuite mémoire >>> def leakmem(): ... f = fileinfo.FileInfo('/music/_singles/kairo.mp3') (1) ... >>> for i in range(100): ... leakmem() (2) (1) A chaque fois que la fonction leakmem est appelée, nous créons une instance de FileInfo et l'assignons à la variable f, qui est une variable locale à la fonction. La fonction s'achève sans jamais libérer f, vous pourriez donc vous attendre à une fuite mémoire, mais vous auriez tort. Lorsque la fonction se termine, la variable locale f est hors de portée. A ce moment, il n'y a plus de référence à l'instance nouvellement créée de FileInfo (puisque nous ne l'avons jamais assignée à autre chose qu'à f), Python détruit alors l'instance pour nous. (2) Peu importe le nombre de fois que nous appelons la fonction leakmem, elle ne provoquera jamais de fuite mémoire puisque Python va détruire à chaque fois la nouvelle instance de la classe FileInfo avant le retour de leakmem. Le terme technique pour cette forme de ramasse-miettes est <>. Python maintien une liste des références à chaque instance créée. Dans l'exemple ci-dessus, il n'y avait qu'une référence à l'instance de FileInfo : la variable locale f. Quand la fonction se termine, la variable f sort de la portée, le compteur de référence descend alors à 0 et Python détruit l'instance automatiquement. Dans des versions précédentes de Python, il y avait des situations où le comptage de référence échouait et Python ne pouvait pas nettoyer derrière vous. Si vous créiez deux instances qui se référençaient mutuellement (par exemple une liste doublement chaînée où chaque noeud a un pointeur vers le noeud prochain et le précédent dans la liste), aucune des deux instances n'était jamais détruite car Python pensait (correctement) qu'il y avait toujours une référence à chaque instance. Python 2.0 a une forme additionnele de ramasse-miettes appelée <> qui est assez intelligente pour remarquer ce blocage et nettoyer correctement les références circulaires. En tant qu'ancien étudiant en Philosophie, cela me dérange de penser que les choses disparaissent quand personne ne les regarde, mais c'est exactement ce qui se passe en Python. En général, vous pouvez simplement ignorer la gestion mémoire et laisser Python nettoyer derrière vous. Pour en savoir plus sur le ramasse-miettes • La Python Library Reference (http://www.python.org/doc/current/lib/) résume les attributs prédéfinis comme __class__ (http://www.python.org/doc/current /lib/specialattrs.html). • La Python Library Reference (http://www.python.org/doc/current/lib/) documente le module gc module (http://www.python.org/doc/current/lib/ module-gc.html), qui vous donne un contrôle de bas niveau sur le ramasse-miettes de Python. 5.5. UserDict : une classe enveloppe Comme vous l'avez vu, FileInfo est une classe qui se comporte comme un dictionnaire. Pour voir ça plus en profondeur, regardons la classe UserDict dans le module UserDict, qui est l'ancêtre de notre classe FileInfo. Cela n'a rien de spécial, la classe est écrite en Python et stockée dans un fichier .py, tout comme notre code. En fait, elle est stockée dans le répertoire lib de votre installation Python. ASTUCE: Dans l'IDE ActivePython sous Windows, vous pouvez ouvrir rapidement n'importe quel module dans votre chemin de bibliothèques avec File-> Locate... (Ctrl-L). Exemple 5.9. Definition de la classe UserDict class UserDict: (1) def __init__(self, dict=None): (2) self.data = {} (3) if dict is not None: self.update(dict) (4) (5) (1) Notez que UserDict est une classe de base, elle n'hérite d'aucune classe. (2) Voici la méthode __init__ que nous avons redéfini dans la classe FileInfo. Notez que la liste d'arguments dans cette classe ancêtre est différente de celle du descendant. Cela ne pose pas de problème, chaque classe dérivée peut avoir sa propre liste d'arguments, tant qu'elle appelle la méthode de l'ancêtre avec les arguments corrects. Ici, la classe ancêtre a un moyen de définir des valeurs initiales (en passant un dictionnaire à l'argument dict) ce que notre FileInfo n'exploite pas. (3) Python supporte les données attributs (appelés <> en Java et Powerbuilder, <> en C++), qui sont des données propres à une instance spécifique de la classe. Dans ce cas, chaque instance de UserDict aura un attribut de données data. Pour référencer cet attribut depuis du code extérieur à la classe, vous devez le qualifier avec le nom de l'instance, instance.data, de la même manière que vous qualifiez une fonction avec son nom de module. Pour référencer un attribut de données depuis la classe, nous utilisons self pour le qualifier. Par convention, tous les données attributs sont initalisées à des valeurs raisonnables dans la méthode __init__. Cependant, ce n'est pas obligatoire, puisque les données attributs, comme les variables locales viennent à existence lorsqu'on leur assigne une valeur pour la première fois. (4) La méthode update est un duplicateur de dictionnaire. Elle copie toutes les clés et valeurs d'un dictionnaire à l'autre. Cela n'efface pas le dictionnaire de destination si il a déjà des clés, celles qui sont présente dans le dictionnaire source seront récrites, mais les autres ne seront pas touchées. Considérez update comme une fonction de fusion, pas de copie. (5) Voici une syntaxe que vous n'avez peut-être pas vu auparavant (je ne l'ai pas employé dans les exemples de ce livre). C'est une instruction if, mais au lieu d'avoir un bloc indenté commençant à la ligne suivante, il y a juste une instruction unique sur la même ligne après les deux points. C'est une syntaxe tout à fait légale, c'est juste un raccourci lorsque vous n'avez qu'une instruction dans un bloc (comme donner une instruction unique sans accolades en C++). Vous pouvez employer cette syntaxe ou vous pouvez avoir du code indenté sur les lignes suivantes, mais vous ne pouvez pas mélanger les deux dans le même bloc. NOTE: Python vs. Java : surcharge de fonction Java et Powerbuilder supportent la surcharge de fonction par liste d'arguments : une classe peut avoir différentes méthodes avec le même nom mais avec un nombre différent d'arguments ou des arguments de type différent. D'autres langages (notamment PL/SQL) supportent même la surcharge de fonction par nom d'argument : une classe peut avoir différentes méthodes avec le même nom et le même nombre d'arguments du même type mais avec des noms d'arguments différents. Python ne supporte ni l'une ni l'autre, il n'a tout simplement aucune forme de surcharge de fonction. Les méthodes sont définies uniquement par leur nom et il ne peut y avoir qu'une méthode par classe avec le même nom. Donc si une classe descendante a une méthode __init__, elle redéfinit toujours la méthode __init__ de la classe ancêtre, même si la descendante la définit avec une liste d'arguments différente. Et la même règle s'applique pour toutes les autres méthodes. NOTE: Guido, l'auteur originel de Python, explique la redéfinition de méthode de cette manière : <> Si cela n'a pas de sens pour vous (personellement, je m'y perd complètement) vous pouvez ignorer la question. Je me suis juste dit que je ferais circuler l'information. Attention: Assignez toujours une valeur initiale à toutes les données attributs d'une instance dans la méthode __init__. Cela vous épargera des heures de débogage plus tard, à la poursuite d'exceptions AttributeError pour cause de référence à des attributs non-initialisés (et donc non-existants). Exemple 5.10. Méthodes ordinaires de UserDict def clear(self): self.data.clear() (1) def copy(self): (2) if self.__class__ is UserDict: (3) return UserDict(self.data) import copy (4) return copy.copy(self) def keys(self): return self.data.keys() (5) def items(self): return self.data.items() def values(self): return self.data.values() (1) clear est une méthode de classe ordinaire, elle est disponible publiquement et peut être appelée par n'importe qui. Notez que clear, comme toutes les méthodes de classe, a pour premier argument self (rappelez-vous que vous ne mentionnez pas self lorsque vous appelez la méthode, Python l'ajoute pour vous). Notez aussi la technique de base employé par cette classe enveloppe : utiliser un véritable dictionnaire (data) comme données attributs, définir toutes les méthodes d'un véritable dictionnaire et rediriger chaque méthode vers la méthode du véritable dictionnaire (au cas où vous l'auriez oublié, la méthode clear d'un dictionnaire supprime toutes ses clés et leurs valeurs associées). (2) La méthode copy d'un véritable dictionnaire retourne un nouveau dictionnaire qui est un double exact de l'original (avec les mêmes paires clé-valeur). Mais UserDict ne peut pas simplement rediriger la méthode vers self.data.copy, car cette méthode retourne un véritable dictionnaire, alors que nous voulons retourner une nouvelle instance qui soit de la même classe que self. (3) Nous utilisons l'attribut __class__ pour voir si self est un UserDict et, dans ce cas, tout va bien puisque nous savons comment copier un UserDict : il suffit de créer un nouveau UserDict et de lui passer le dictionnaire véritable qu'est self.data. (4) Si self.__class__ n'est pas un UserDict, alors self doit être une classe dérivée de UserDict (par exemple FileInfo), dans ce cas c'est plus compliqué. UserDict ne sait pas comment faire une copie exacte d'un de ses descendants. Il pourrait y avoir, par exemple, d'autres données attributs définies dans la classe dérivée, ce qui nécessiterait de les copier tous. Heureusement Python est fourni avec un module qui remplit cette tâche, le module copy. Je ne vais pas entrer ici dans les détails (bien que ce soit très intéressant et vaille la peine que vous y jetiez un coup d' il). Il suffit de dire que copy peut copier un objet Python quelconque et que c'est comme cela que nous l'employons ici. (5) Le reste des méthodes est sans difficulté, les appels sont redirigés vers les méthodes de self.data. NOTE: Note historique Dans les versions de Python antérieures à la 2.2, vous ne pouviez pas directement dériver les types de données prédéfinis comme les chaînes, les listes et les dictionnaires. Pour compenser cela, Python est fourni avec des classes enveloppes qui reproduisent le comportement de ces types de données prédéfinis : UserString, UserList et UserDict. En utilisant un mélange de méthodes ordinaires et spéciales, la classe UserDict fait une excellente imitation d'un dictionnaire, mais c'est juste une classe comme les autres, vous pouvez donc la dériver pour créer des classes personalisées semblables à un dictionnaire comme FileInfo. En Python 2.2 et suivant, vous pourriez récrire l'exemple de ce chapitre de manière à ce que FileInfo hérite directement de dict au lieu de UserDict. Cependant, vous devriez quand même lire l'explication du fonctionnement de UserDict au cas où vous auriez besoin d'implémenter ce genre d'objet enveloppe ou au cas où vous auriez à travailler avec une version de Python antérieure à la 2.2. En Python il est possible de dériver une classe directement du type de données prédéfini dict, comme dans l'exemple suivant. Il y a trois différence avec la version dérivée de UserDict. Exemple 5.11. Dériver une classe directement du type prédéfini dict class FileInfo(dict): (1) "store file metadata" def __init__(self, filename=None): (2) self["name"] = filename (1) La première différence est que nous n'avons pas besoin d'importer le module UserDict, puisque dict est un type prédéfini et donc toujours disponible. La seconde est que nous dérivons notre classe de dict directement et non de UserDict.UserDict. (2) La troisième différence est subtile mais importante. À cause de la manière dont UserDict fonctionne en interne, nous devons appeler explicitement sa méthode __init__ pour l'initialiser correctement. dict ne fonctionne pas de la même manière, ce n'est pas une enveloppe et il ne demande pas d'initialisation explicite. Pour en savoir plus sur UserDict • La Python Library Reference (http://www.python.org/doc/current/lib/) documente le module UserDict (http://www.python.org/doc/current/lib/ module-UserDict.html) et le module copy (http://www.python.org/doc/current/ lib/module-copy.html). 5.6. Méthodes de classe spéciales En plus des méthodes de classe ordinaires, il y a un certain nombre de méthodes spéciales que les classes Python peuvent définir. Au lieu d'être appelées directement par votre code (comme les méthodes ordinaires) les méthodes spéciales sont appelées pour vous par Python dans des circonstances particulières ou quand une syntaxe spécifique est utilisée. Comme vous l'avez vu dans la section précédente, les méthodes ordinaires nous ont permis en grande partie d'envelopper un dictionnaire dans une classe. Mais les méthodes ordinaires seules ne suffisent pas parce qu'il y a beaucoup de choses que vous pouvez faire avec un dictionnaire en dehors d'appeler ses méthodes. Pour commencer vous pouvez lire (get) et écrire (set) des éléments à l'aide d'une syntaxe qui ne fait pas explicitement appel à des méthodes. C'est là que les méthodes de classe spéciales interviennent : elle fournissent un moyen de faire correspondre la syntaxe n'appelant pas de méthodes à des appels de méthodes. 5.6.1. Lire et écrire des éléments Exemple 5.12. La méthode spéciale __getitem__ def __getitem__(self, key): return self.data[key] >>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3") >>> f {'name':'/music/_singles/kairo.mp3'} >>> f.__getitem__("name") (1) '/music/_singles/kairo.mp3' >>> f["name"] (2) '/music/_singles/kairo.mp3' (1) La méthode spéciale __getitem__ à l'air simple. Comme les méthodes ordinaires clear, keys et values, elle ne fait que rediriger vers le dictionnaire pour obtenir sa valeur. Mais comment est-elle appelée ? Vous pouvez appeler __getitem__ directement, mais en pratique vous ne le ferez pas, je le fais ici seulement pour vous montrer comment ça marche. La bonne manière d'utiliser __getitem__ est d'obtenir de Python qu'il fasse l'appel pour vous. (2) Cela à l'apparence exacte de la syntaxe que l'on utilise pour obtenir une valeur d'un dictionnaire et cela retourne bien la valeur que l'on attend. Mais il y a un chaînon manquant : en coulisse, Python convertit cette syntaxe en un appel de méthode f.__getitem__("name"). C'est pourquoi __getitem__ est une méthode de classe spéciale, non seulement vous pouvez l'appeler vous-même, mais vous pouvez faire en sorte que Python l'appelle pour vous grâce à la syntaxe appropriée. Bien sûr, Python a une méthode spéciale __setitem__ pour accompagner __getitem__, comme nous le montrons dans l'exemple suivant. Exemple 5.13. La méthode spéciale __setitem__ def __setitem__(self, key, item): self.data[key] = item >>> f {'name':'/music/_singles/kairo.mp3'} >>> f.__setitem__("genre", 31) (1) >>> f {'name':'/music/_singles/kairo.mp3', 'genre':31} >>> f["genre"] = 32 (2) >>> f {'name':'/music/_singles/kairo.mp3', 'genre':32} (1) Comme la méthode __getitem__, __setitem__ redirige simplement l'appel au véritable dictionnaire self.data. Et comme pour __getitem__, vous n'appelez pas directement cette méthode en général, Python appelle __setitem__ pour vous lorsque vous utilisez la bonne syntaxe. (2) Cela ressemble à la syntaxe habituelle d'utilisation d'un dictionnaire, mais en fait f est une classe faisant de son mieux pour passer pour un dictionnaire et __setitem__ est un élément essentiel de cette apparence. Cette ligne de code appelle en fait f.__setitem__("genre", 32) en coulisse. __setitem__ est une méthode de classe spéciale car elle est appelée pour vous, mais c'est quand même une méthode de classe. Nous pouvons la redéfinir dans une classe descendante tout aussi facilement qu'elle a été définie dans UserDict. Cela nous permet de définir des classes qui se comportent en partie comme des dictionnaires, mais qui ont leur propre comportement dépassant le cadre d'un simple dictionnaire. Ce concept est à la base de tout le framework que nous étudions dans ce chapitre. Chaque type de fichier peut avoir une classe de manipulation qui sait comment obtenir des méta-données d'un type particulier de fichier. Une fois certains attributs (comme le nom et l'emplacement du fichier) connus, la classe de manipulation sait comment obtenir les autres attributs automatiquement. Cela se fait en redéfinissant la méthode __setitem__, en cherchant des clés particulières et en ajoutant un traitement supplémentaire quand elles sont trouvées. Par exemple, MP3FileInfo est un descendant de FileInfo. Quand le nom (name) d'un MP3FileInfo est défini, cela ne change pas seulement la valeur de la clé name (comme pour l'ancêtre FileInfo), mais déclenche la recherche de balises MP3 et définit tout un ensemble de clés. Exemple 5.14. Redéfinition de __setitem__ dans MP3FileInfo def __setitem__(self, key, item): (1) if key == "name" and item: (2) self.__parse(item) (3) FileInfo.__setitem__(self, key, item) (4) (1) Notez que notre méthode __setitem__ est définie exactement comme la méthode héritée. C'est important car Python appellera la méthode pour nous et qu'il attend un certain nombre d'arguments (techniquement parlant, les noms des arguments n'ont pas d'importance, seulement leur nombre). (2) Voici le point crucial de toute la classe MP3FileInfo : si nous assignons une valeur à la clé name, alors nous voulons faire quelque chose en plus. (3) Le traitement supplémentaire que nous faisons pour les noms (name) est encapsulé dans la méthode __parse. C'est une autre méthode de classe définie dans MP3FileInfo et quand nous l'appelons nous la qualifions avec self. Un appel à __parse tout court chercherait une fonction ordinaire définie hors de la classe, ce qui n'est pas ce que nous voulons, appeler self.__parse cherchera une méthode définie dans la classe. Cela n'a rien de nouveau, c'est de la même manière que l'on fait référence aux données attributs. (4) Après avoir fait notre traitement supplémentaire, nous voulons appeler la méthode héritée. Rappelez-vous que Python ne le fait jamais pour vous, vous devez le faire manuellement. Notez que nous appelons l'ancêtre immédiat, FileInfo, même si il n'a pas de méthode __setitem__. Cela fonctionne parce que Python va remonter la hierarchie d'heritage jusqu'à ce qu'il trouve une classe avec la méthode que nous appelons, cette ligne finira donc par trouver et appeler la méthode __setitem__ définie dans UserDict. NOTE: Lorsque vous accédez à des données attributs dans une classe, vous devez qualifier le nom de l'attribut : self.attribute. Lorsque vous appelez d'autres méthodes dans une classe, vous devez qualifier le nom de la méthode : self.method. Exemple 5.15. Setting an MP3FileInfo's name >>> import fileinfo >>> mp3file = fileinfo.MP3FileInfo() (1) >>> mp3file {'name':None} >>> mp3file["name"] = "/music/_singles/kairo.mp3" (2) >>> mp3file {'album': 'Rave Mix', 'artist': '***DJ MARY-JANE***', 'genre': 31, 'title': 'KAIRO****THE BEST GOA', 'name': '/music/_singles/kairo.mp3', 'year': '2000', 'comment': 'http://mp3.com/DJMARYJANE'} >>> mp3file["name"] = "/music/_singles/sidewinder.mp3" (3) >>> mp3file {'album': '', 'artist': 'The Cynic Project', 'genre': 18, 'title': 'Sidewinder', 'name': '/music/_singles/sidewinder.mp3', 'year': '2000', 'comment': 'http://mp3.com/cynicproject'} (1) D'abord nous créons une instance de MP3FileInfo sans lui passer de nom de fichier (nous pouvons le faire parce que l'argument filename de la méthode __init__ est optionnel). Comme MP3FileInfo n'a pas de méthode __init__ propre, Python remonte la hierarchie d'héritage et trouve la méthode __init__ de FileInfo. Cette méthode __init__ appelle manuellement la méthode __init__ de UserDict puis définit la clé name à la valeur de filename, qui est None puisque nous n'avons passé aucun nom de fichier. Donc mp3file est au début un dictionnaire avec une clé, name, dont la valeur est None. (2) Maintenant les choses sérieuses commencent. Définir la clé name de mp3file déclenche la méthode __setitem__ de MP3FileInfo (pas UserDict), qui remarque que nous définissons la clé name avec une valeur réelle et appelle self.__parse. Bien que nous n'ayons pas encore vu le contenu de la méthode __parse, vous pouvez voir à partir de la sortie qu'elle définit plusieurs autres clés : album, artist, genre, title, year et comment. (3) Modifier la clé name recommencera le même processus : Python appelle __setitem__, qui appelle self.__parse, qui définit toutes les autres clés. 5.7. Méthodes spéciales avancées Il y a d'autres méthodes spéciales que __getitem__ et __setitem__. Certaines vous laissent émuler des fonctionnalité dont vous ignorez encore peut-être tout. Cet exemple montre certaines des autres méthodes spéciales de UserDict. Exemple 5.16. D'autres méthodes spéciales dans UserDict def __repr__(self): return repr(self.data) (1) def __cmp__(self, dict): (2) if isinstance(dict, UserDict): return cmp(self.data, dict.data) else: return cmp(self.data, dict) def __len__(self): return len(self.data) (3) def __delitem__(self, key): del self.data[key] (4) (1) __repr__ est une méthode spéciale qui est appelée lorsque vous appelez repr (instance). La fonction repr est une fonction prédéfinie qui retourne une représentation en chaîne d'un objet. Elle fonctionne pour tout objet, pas seulement les instances de classes. En fait, vous êtes déjà familier de repr, même si vous l'ignorez. Dans la fenêtre interactive, lorsque vous tapez juste un nom de variable et faites Entrée, Python utilise repr pour afficher la valeur de la variable. Créez un dictionnaire d avec des données, puis faites print repr(d) pour le voir par vous même. (2) __cmp__ est appelé lorsque vous comparez des instances de classe. En général, vous pouvez comparer deux objets Python quels qu'ils soient, pas seulement des instances de classe, en utilisant ==. Il y a des règles qui définissent quand les types de données prédéfinis sont considérés égaux. Par exemple, les dictionnaires sont égaux quand ils ont les mêmes clés et valeurs, les chaînes sont égales quand elles ont la même longueur et contiennent la même séquence de caractères. Pour les instances de classe, vous pouvez définir la méthode __cmp__ et écrire la logique de comparaison vous-même et vous pouvez ensuite utiliser == pour comparer des instances de votre classe, Python appelera votre méthode spéciale __cmp__ pour vous. (3) __len__ est appelé lorsque vous appelez len(instance). La fonction len est une fonction prédéfinie qui retourne la longueur d'un objet. Elle fonctionne pour tout objet pour lequel il est envisageable de penser qu'il a une longueur. La len d'une chaîne est son nombre de caractères, la len d'un dictionnaire est son nombre de clés et la len d'une liste ou tuple est son nombre d'éléments. Pour les instances de classe, définissez la méthode __len__ et écrivez le calcul de longueur vous-même, puis appelez len (instance) et Python appelera votre méthode spéciale __len__ pour vous. (4) __delitem__ est appelé lorsque vous appelez del instance[key], ce qui, vous vous en rappelez peut-être, est le moyen de supprimer des éléments individuels d'un dictionnaire. Quand vous utilisez del sur une instance de classe, Python appelle la méthode spéciale __delitem__ pour vous. NOTE: Python vs. Java : égalité et identité En Java, vous déterminez si deux variables de chaînes référencent la même zone mémoire à l'aide de str1 == str2. On appelle cela identité des objets et la syntaxe Python en est str1 is str2. Pour comparer des valeurs de chaînes en Java, vous utiliseriez str1.equals(str2), en Python, vous utiliseriez str1 == str2. Les programmeurs Java qui ont appris que le monde était rendu meilleur par le fait que == en Java fasse une comparaison par identité plutôt que par valeur peuvent avoir des difficultés à s'adapter au fait que Python est dépourvu d'un tel piège. Vous trouvez peut-être que ça fait beaucoup de travail pour faire avec une classe ce qu'on peut faire avec un type de données prédéfini. Et c'est vrai que tout serait plus simple (et la classe UserDict serait inutile) si on pouvait hériter d'un type de données prédéfini comme un dictionnaire. Mais même si vous pouviez le faire, les méthodes spéciales seraient toujours utiles, car elles peuvent être utilisées dans n'importe quelle classe, pas seulement dans une classe enveloppe comme UserDict. Les méthodes spéciales permettent à toute classe de stocker des paires clé-valeur comme un dictionnaire, simplement en définissant la méthode __setitem__. Toute classe peut se comporter comme une séquence, simplement en définissant la méthode __getitem__. Toute classe qui définit la méthode __cmp__ peut être comparée avec ==. Et si votre classe représente quelque chose qui a une longeur, ne créez pas une méthode GetLength, définissez la méthode __len__ et utilisez len(instance). NOTE: Alors que les autres langages orientés objet ne vous laissent définir que le modèle physique d'un objet (<>), les méthodes spéciales de Python comme __len__ vous permettent de définir le modèle logique d'un objet (<>). Il y a de nombreuses autres méthodes spéciales. Un ensemble de ces méthodes permet aux classes de se comporter comme des nombres, permettant l'addition, la soustraction et autres opérations arithmétiques sur des instances de classe (l'exemple type en est une classe représentant les nombres complexes, nombres ayant à la fois un composant réel et imaginaire). La méthode __call__ permet à une classe de se comporter comme une fonction, ce qui permet d'appeler une instance de classe directement. Il y a aussi d'autres méthodes spéciales permettant aux classes d'avoir des données attributs en lecture seule ou en écriture seule, nous en parlerons dans des chapitres à venir. Pour en savoir plus sur les méthodes de classe spéciales • La Python Reference Manual (http://www.python.org/doc/current/ref/) documente toutes les méthodes spéciales de classe (http://www.python.org/ doc/current/ref/specialnames.html). 5.8. Attributs de classe Vous connaissez déjà les données attributs, qui sont des variables appartenant à une instance particulière d'une classe. Python permet aussi les attributs de classe, qui sont des variables appartenant à la classe elle-même. Exemple 5.17. Présentation des attributs de classe 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)} >>> import fileinfo >>> fileinfo.MP3FileInfo (1) >>> fileinfo.MP3FileInfo.tagDataMap (2) {'title': (3, 33, ), 'genre': (127, 128, ), 'artist': (33, 63, ), 'year': (93, 97, ), 'comment': (97, 126, ), 'album': (63, 93, )} >>> m = fileinfo.MP3FileInfo() (3) >>> m.tagDataMap {'title': (3, 33, ), 'genre': (127, 128, ), 'artist': (33, 63, ), 'year': (93, 97, ), 'comment': (97, 126, ), 'album': (63, 93, )} (1) MP3FileInfo est la classe elle-même, pas une instance particulière de cette classe. (2) tagDataMap est un attribut de classe : littéralement, un attribut de la classe. Il est diponible avant qu'aucune instance de la classe n'ait été créée. (3) Les attributs de classe sont disponibles à la fois par référence directe à la classe et par référence à une instance quelconque de la classe. NOTE: Python vs. Java définition des attributs En Java, les variables statiques (appelées attributs de classe en Python) aussi bien que les variables d'instance (appelées données attributs en Python) sont définies immédiatement après la définition de la classe (avec le mot-clé static pour les premières). En Python, seuls les attributs de classe peuvent être définis à cet endroit, les données attributs sont définies dans la méthode __init__. Les attributs de classe peuvent être utilisés comme des constantes au niveau de la classe (ce qui est la manière dont nous les utilisons dans MP3FileInfo), mais ils ne sont pas vraiment des constantes. Vous pouvez également les modifier. NOTE: Il n'y a pas de constantes en Python. Tout est modifiable en faisant un effort. C'est en accord avec un des principes essentiels de Python : un mauvais comportement doit être découragé mais pas interdit. Si vous voulez vraiment changer la valeur de None, vous pouvez le faire, mais ne venez pas vous plaindre que votre code est impossible à déboguer. Exemple 5.18. Modification des attributs de classe >>> class counter: ... count = 0 (1) ... def __init__(self): ... self.__class__.count += 1 (2) ... >>> counter >>> counter.count (3) 0 >>> c = counter() >>> c.count (4) 1 >>> counter.count 1 >>> d = counter() (5) >>> d.count 2 >>> c.count 2 >>> counter.count 2 (1) count est un attribut de la classe counter. (2) __class__ est un attribut prédéfini de toute instance de classe (de toute classe). C'est une référence à la classe dont self est une instance (dans ce cas, la classe counter). (3) Comme count est un attribut de classe, il est disponible par référence directe à la classe, avant que nous ayions créé une instance de la classe. (4) Créer une instance de la classe appelle la méthode __init__, qui incrémente l'attribut de classe count de 1. Cela affecte la classe elle-même, pas seulement l'instance nouvellement créée. (5) Créer une seconde instance incrémente à nouveau l'attribut de classe count. Vous constatez que l'attribut de classe est partagé par la classe et toutes ses instances. 5.9. Fonctions privées Comme la plupart des langages, Python possède le concept d'éléments privées : • des fonctions privées, qui ne peuvent être appelées de l'extérieur de leur module ; • des méthodes de classe privées, qui ne peuvent être appelées de l'extérieur de leur classe ; • des attributs privés, qui ne peuvent être accédé de l'extérieur de leur classe. Contrairement à la plupart des langages, le caractère privé ou public d'une fonction, d'une méthode ou d'un attribut est déterminé en Python entièrement par son nom. Si le nom d'une fonction, d'une méthode de classe ou d'un attribut commence par (mais ne se termine pas par) deux caractères de soulignement il s'agit d'un élément privé, tout le reste est public. Python ne possède pas de concept de méthode protégée (accessible seulement à la même classe ou a ses descendants). Les méthodes d'une classe sont ou privées (accessibles seulement à la même classe) ou publiques (accessibles à tout le monde). MP3FileInfo a deux méthodes : __parse et __setitem__. Comme nous en avons déjà discuté, __setitem__ est une méthode spéciale, vous l'appelez normalement indirectement en utilisant la syntaxe de dictionnaire sur une instance de classe, mais elle est publique et vous pouvez l'appeler directement (même d'en dehors du module fileinfo) si vous avez une bonne raison de le faire. Par contre __parse est privée, car elle a deux caractères de soulignement au début de son nom. NOTE: Convention pour les noms de méthodes En Python, toutes les méthodes spéciales (comme __setitem__) et les attributs prédéfinis (comme __doc__) suivent une convention standard : il commencent et se terminent par deux caractères de soulignement. Ne nommez pas vos propres méthodes et attributs de cette manière, cela n'apporterait que de la confusion pour vous et les autres. Exemple 5.19. Tentative d'appel d'une méthode privée >>> import fileinfo >>> m = fileinfo.MP3FileInfo() >>> m.__parse("/music/_singles/kairo.mp3") (1) Traceback (innermost last): File "", line 1, in ? AttributeError: 'MP3FileInfo' instance has no attribute '__parse' (1) Si vous essayez d'appeler une méthode privée, Python lévera une exception un peu trompeuse disant que la méthode n'existe pas. Bien sûr elle existe, mais elle est privée, donc elle n'est pas accessible d'en dehors de la classe. A proprement parler, les méthodes privées sont accessibles d'en dehors de leur classe, mais pas facilement accessibles. Rien n'est vraiment privé en Python, en interne les noms des méthodes et attributs privés sont camouflés à la volée pour les rendre inaccessibles par leur nom d'origine. Vous pouvez accéder à la méthode __parse de la classe MP3FileInfo par le nom _MP3FileInfo__parse. Vous êtes prié de trouver ça intéressant mais de promettre de ne jamais, jamais l'utiliser dans votre code. Les méthodes privées ont une bonne raison de l'être, mais comme beaucoup d'autres choses en Python, leur caractère privé est en dernière instance une question de convention et non d'obligation. Pour en savoir plus sur les fonctions privées • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite du fonctionnement des variables privées (http://www.python.org/doc/current/ tut/node11.html#SECTION0011600000000000000000). 5.10. Résumé Voila pour ce qui est des chicanes techniques des objets. Vous verrez une application réelle des méthodes de classe spéciales au Chapitre 12, qui utilise getattr pour créer un mandataire d'un service web distant. Le prochain chapitre continuera d'utiliser ce programme d'exemple pour explorer d'autres concepts de Python comme les exceptions, les objets-fichiers et les boucles for. Avant de plonger dans le prochain chapitre, assurez-vous que vous vous sentez à l'aise pour : • Importer des modules en utilisant import module ou from module import • Definir et instancier des classes • Definir des méthodes __init__ et autres méthodes spéciales et comprendre quand elles sont appelées • Dériver de UserDict pour définir des classes qui se comportent comme des dictionnaires • Définir des données attributs et des attributs de classe et comprendre la différence entre les deux • Définir des méthodes privées Chapitre 6. 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. 6.1. Traitement des exceptions Comme beaucoup de langages orientés objet, Python gère les exception à l'aide de blocs try...except. NOTE: Python vs. Java : traitement des exceptions 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 >>> fsock = open("/notthere", "r") (1) Traceback (innermost last): File "", 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. 6.1.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 <> 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 # 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 • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite de la définition et du déclenchement de vos propres exceptions et de la gestion de plusieurs exceptions à la fois (http://www.python.org/doc/ current/tut/node10.html#SECTION0010400000000000000000). • La Python Library Reference (http://www.python.org/doc/current/lib/) résume toutes les exceptions prédéfinies (http://www.python.org/doc/current/lib/ module-exceptions.html). • La Python Library Reference (http://www.python.org/doc/current/lib/) documente le module getpass (http://www.python.org/doc/current/lib/ module-getpass.html). • La Python Library Reference (http://www.python.org/doc/current/lib/) documente le module traceback (http://www.python.org/doc/current/lib/ module-traceback.html), qui fournit un accès de bas niveau aux attributs d'une exception après qu'elle ait été déclenchée. • La Python Reference Manual (http://www.python.org/doc/current/ref/) traite du fonctionnement interne du bloc try...except (http://www.python.org/doc/ current/ref/try.html). 6.2. 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 >>> f = open("/music/_singles/kairo.mp3", "rb") (1) >>> f (2) >>> 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. 6.2.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 >>> f >>> 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. 6.2.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 >>> f >>> f.closed (1) False >>> f.close() (2) >>> f >>> f.closed (3) True >>> f.seek(0) (4) Traceback (innermost last): File "", line 1, in ? ValueError: I/O operation on closed file >>> f.tell() Traceback (innermost last): File "", line 1, in ? ValueError: I/O operation on closed file >>> f.read() Traceback (innermost last): File "", 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. 6.2.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 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é <>, quoi qu'il se soit passé <>. (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, <> 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 try...except. 6.2.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 <> Il suffit d'ouvrir le fichier pour commencer à y écrire. Exemple 6.7. Ecriture dans un fichier >>> 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 • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite de la lecture et de l'écriture de fichiers, y compris comment lire un fichier ligne par ligne dans une liste (http://www.python.org/doc/current/ tut/node9.html#SECTION009210000000000000000). • eff-bot (http://www.effbot.org/guides/) traite de l'efficience et de la performance de différentes manières de lire un fichier (http:// www.effbot.org/guides/readline-performance.htm). • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) répond aux questions courantes à propose des fichiers (http:// www.faqts.com/knowledge-base/index.phtml/fid/552). • La Python Library Reference (http://www.python.org/doc/current/lib/) résume toutes les méthodes de l'objet-fichier (http://www.python.org/doc/current/ lib/bltin-file-objects.html). 6.3. 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 >>> 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 <> (selon les critères de Visual Basic) pour la boucle for est également simple. Exemple 6.9. Compteurs simples >>> 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 >>> 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 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, )), 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. 6.4. 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 >>> 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 >>> 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 >>> sys.modules["fileinfo"] (2) (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__ >>> from fileinfo import MP3FileInfo >>> MP3FileInfo.__module__ (1) 'fileinfo' >>> sys.modules[MP3FileInfo.__module__] (2) (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 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 <>. Pour en savoir plus • Le Python Tutorial (http://www.python.org/doc/current/tut/tut.html) traite de quand et comment les arguments par défaut sont évalués (http:// www.python.org/doc/current/tut/node6.html#SECTION006710000000000000000). • La Python Library Reference (http://www.python.org/doc/current/lib/) documente le module sys (http://www.python.org/doc/current/lib/ module-sys.html). 6.5. 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 >>> 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 >>> 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 >>> 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 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. NOTE: 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 >>> 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 • La Python Knowledge Base (http://www.faqts.com/knowledge-base/index.phtml/ fid/199/) répond aux questions sur le module os (http://www.faqts.com/ knowledge-base/index.phtml/fid/240). • La Python Library Reference (http://www.python.org/doc/current/lib/) documente le module os (http://www.python.org/doc/current/lib/ module-os.html) et le module os.path (http://www.python.org/doc/current/lib /module-os.path.html). 6.6. 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 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 <> (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. 6.7. Résumé Le programme fileinfo.py, introduit au Chapitre 5; devrait maintenant être parfaitement clair. """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 Chapitre 7. Expressions régulières Les expressions régulières sont un moyen puissant et standardisé de rechercher, remplacer et analyser du texte à l’aide de motifs complexes de caractères. Si vous avez utilisé les expressions régulières dans d’autres langages (comme Perl), vous pouvez sauter cette section et lire uniquement la présentation du module re (http://www.python.org/doc/current/lib/module-re.html) pour avoir une vue d’ensemble des fonctions disponibles et de leurs arguments. 7.1. Plonger Les objets-chaîne Strings ont des méthodes pour rechercher (index, find et count), remplacer (replace) et analyser (split) mais elles sont limitées aux cas les plus simples. Les méthodes de recherche tentent de trouver une chaîne unique et prédéfinie et elles sont toujours sensibles à la casse. Pour faire une recherche non sensible à la casse sur une chaîne s, vous devez appeler s.lower() ou s.upper() et vous assurer que vos chaînes de recherche sont dans la casse correspondante. Les méthodes replace et split ont les mêmes restrictions. Si ce que vous essayez de faire peut être accompli avec les fonctions de chaînes, utilisez-les. Elles sont rapides et faciles à comprendre et il y a beaucoup d'avantages à un code rapide, simple et lisible. Mais si vous vous rendez compte que vous utilisez un grand nombre de fonctions de chaînes différentes avec des instruction if pour les cas particulier, ou si vous les associez à des fonctions split et join et à des list comprehension de manière complexe et illisible, vous devez vous tourner vers les expressions régulières. Bien que la syntaxe des expressions régulières soit compacte et différente du code ordinaire, le résultat peut être plus lisible qu’une solution à la main avec une longue séquence de fonctions de chaînes. Il y a même une manière d’inclure des commentaires dans les expressions régulières pour les documenter. 7.2. Exemple : adresses postales Cette série d’exemples est inspirée d’un problème réel que j’ai eu au cours de mon travail, l’extraction et la standardisation d’adresses postales exportées d’un ancien système avant de les importer dans un nouveau système (vous voyez, je n’invente rien, c’est réellement utile). L’exemple suivant montre comment j’ai abordé ce problème. Exemple 7.1. Reconnaître la fin d’une chaîne >>> s = '100 NORTH MAIN ROAD' >>> s.replace('ROAD', 'RD.') (1) '100 NORTH MAIN RD.' >>> s = '100 NORTH BROAD ROAD' >>> s.replace('ROAD', 'RD.') (2) '100 NORTH BRD. RD.' >>> s[:-4] + s[-4:].replace('ROAD', 'RD.') (3) '100 NORTH BROAD RD.' >>> import re (4) >>> re.sub('ROAD$', 'RD.', s) (5) (6) '100 NORTH BROAD RD.' (1) Mon but était de standardiser les adresses de manière à ce que 'ROAD' soit toujours abrégé en 'RD.'. Au premier abord, je pensais que ce serait assez simple pour utiliser uniquement la méthode de chaîne replace. Après tout, toutes les données étaient déjà en majuscules, donc les erreurs de casses ne seraient pas un problème. De plus, la chaîne de recherche, 'ROAD', était une constante. Pour cet exemple trompeusement simple, s.replace fonctionne effectivement. (2) Malheureusement, la vie est pleine de contre-exemples et je découvrais assez rapidemment celui-ci. Le problème ici est que 'ROAD' apparaît deux fois dans l’adresse, d’abord comme partie du nom de la rue 'BROAD' et ensuite comme mot isolé. La méthode replace trouve ces deux occurences et les remplace aveuglément, rendant l’adresse illisible. (3) Pour résoudre le problème des adresses comprenant plus d’une sous-chaîne 'ROAD', nous pourrions recourir à quelque chose de ce genre : ne rechercher et remplacer 'ROAD' que dans les 4 derniers caractères de l’adresse (s [-4:]) et ignorer le début de la chaîne (s[:-4]). Mais on voit bien que ça commence à être embrouillé. Par exemple, le motif dépend de la longueur de la chaîne que nous remplaçons (si nous remplaçons 'STREET' par 'ST.', nous devons écrire s[:-6] et s[-6:].replace(...)). Aimeriez-vous revenir à ce code dans six mois et devoir le débugger ? En ce qui me concerne, certainement pas. (4) Il est temps de recourir aux expressions régulières. En Python, toutes les fonctionalités en rapport aux expressions régulières sont contenues dans le module re. (5) Regardez le premier paramètre, 'ROAD$'. C’est une expression régulière très simple qui ne reconnaît 'ROAD' que s’il apparaît à la fin d’une chaîne. Le symbole $ signifie <> (il y a un caractère correspondant, l’accent circonflexe ^, qui signifie <>). (6) A l’aide de la fonction re.sub, nous recherchons dans la chaîne s l’expression régulière 'ROAD$' et la remplaçons par 'RD.'. Cela correspond à ROAD à la fin de la chaîne s, mais ne correspond pas au ROAD faisant partie du mot BROAD, puisqu’il est au milieu de s. En continuant mon travail de reformatage d’adresses, je decouvrais bientôt que le modèle précédent, reconnaître 'ROAD' à la fin de l’adresse, ne suffisait pas, car toutes les adresses n’incluaient pas d’identifiant pour la rue. Certaines finissaient simplement par le nom de la rue. La plupart du temps, je m’en sortais sans problème, mais si le nom de la rue était 'BROAD', alors l’expression régulière reconnaissait 'ROAD' à la fin de la chaîne dans le mot 'BROAD'. Ce n’était pas ce que je voulais. Exemple 7.2. Reconnaître des mots entiers >>> s = '100 BROAD' >>> re.sub('ROAD$', 'RD.', s) '100 BRD.' >>> re.sub('\\bROAD$', 'RD.', s) (1) '100 BROAD' >>> re.sub(r'\bROAD$', 'RD.', s) (2) '100 BROAD' >>> s = '100 BROAD ROAD APT. 3' >>> re.sub(r'\bROAD$', 'RD.', s) (3) '100 BROAD ROAD APT. 3' >>> re.sub(r'\bROAD\b', 'RD.', s) (4) '100 BROAD RD. APT 3' (1) Ce que je voulais vraiment était de reconnaître 'ROAD' quand il était à la fin de la chaîne et qu’il était un mot isolé, pas une partie de mot. Pour exprimer cela dans une expressions régulière, on utilise \b, qui signifie <>. En Python, c'est rendu plus compliqué par le fait que le caractère '\', qui est le caractère d’échappement, doit lui-même être précédé du caractère d’échappement (c'est ce qui est parfois appelé la backslash plague et c’est une des raison pour lesquelles les expressions régulières sont plus faciles à utliser en Perl qu’en Python. Par contre, Perl mélange les expressions régulières et la syntaxe du langage, donc si vous avez un bogue, il peut être difficile de savoir si c’est une erreur dans la syntaxe ou dans l’expression régulière). (2) Pour éviter la backslash plague, vous pouvez utiliser ce qu’on appelle une chaîne brute, en préfixant la chaîne par la lettre r. Cela signale à Python que cette chaîne doit être traitée sans échappement, '\t' est un caractère de tabulation, mais r'\t' est réellement un caractère backslash \ suivi de la lettre t. Je vous conseille de toujours utiliser des chaînes brutes lorsque vous employez des expressions régulières, sinon cela devient confus très vite (et les expressions régulières peuvent devenir suffisament confuses par elles-mêmes). (3) *soupir* Malheureusement, je découvrais rapidement d’autres cas qui contredisaient mon raisonnement. Dans le cas présent, l’adresse contenait le mot isolé 'ROAD' mais il n’était pas à la fin de la chaîne, car l’adresse avait un numéro d’appartement après l’identifiant de la rue. Comme 'ROAD' n’était pas tout à la fin de la chaîne, il n’était pas identifié, donc l’appel de re.sub s’achèvait sans rien remplacer, j’obtenais en retour la chaîne d’origine, ce qui n’était pas le but recherché. (4) Pour résoudre ce problème, j’enlevais le caractère $et ajoutais un deuxième \b. L’expression régulière signifiait alors <>, que ce soit à la fin, au début ou quelque part au milieu. 7.3. Exemple : chiffres romains Vous avez certainement déjà vu des chiffres romains, par exemple dans Astérix^[ 2] En chiffres romains, il y a sept caractères qui sont répétés et combinés de différentes manières pour représenter des nombres. • I = 1 • V = 5 • X = 10 • L = 50 • C = 100 • D = 500 • M = 1000 Voici les règles générales pour construire des nombres romains : • Les caractères sont additifs. I est 1, II est 2 et III est 3. VI est 6 (littéralement <<5 et 1>>), VII est 7 et VIII est 8. • Les caractères en un (I, X, C, and M) peuvent être répétés jusqu’à trois fois. A 4, vous devez soustraire du prochain caractère en cinq. Vous ne pouvez pas représenter 4 par IIII, au lieu de ça il est représenté par IV (<<1 de moins que 5>>). 40 s’écrit XL (<<10 de moins que 50>>), 41 s’écrit XLI, 42 XLII, 43 XLIII et 44 XLIV (<<10 de moins que 50, puis 1 de moins que 5>>). • De manière similaire, à 9, vous devez soustraire du prochain caractère en un : 8 est VIII mais 9 est IX (<<1 de moins que 10>>), pas VIIII (puisque le caractère I ne peut être répété quatre fois). 90 est XC et 900 CM. • Les caractères en cinq ne peuvent être répétés. 10 est toujours représenté par X, jamais par VV. 100 est toujours C, jamais LL. 7.3.1. Rechercher les milliers Qu’est-ce qui serait nécessaire pour vérifier qu’une chaîne de caractères quelconque constitue des chiffres romains valides ? Nous allons opérer caractère par caractère. Puisque les chiffres romains sont toujours écrits du plus grand vers le plus petit, nous allons commencer par les plus grands : les milliers. Pour les nombres supérieurs à 1000, les milliers sont représentés par une série de caractères M. Exemple 7.3. Rechercher les milliers >>> import re >>> pattern = '^M?M?M?$' (1) >>> re.search(pattern, 'M') (2) >>> re.search(pattern, 'MM') (3) >>> re.search(pattern, 'MMM') (4) >>> re.search(pattern, 'MMMM') (5) >>> re.search(pattern, '') (6) (1) Ce motif a trois parties : □ ^ - reconnaît ce qui suit uniquement en début de chaîne. Si ce n’était pas spécfié, le motif reconnaîtrait les M où qu’ils soient, ce qui n’est pas ce que nous voulons. Nous voulons être sûrs que les M, s’il y en a dans la chaîne, sont à son début. □ M? - reconnaît un M optionnel. Comme nous le répétons trois fois, nous reconnaissons 0 à 3 M se suivant. □ $ - reconnaît ce qui précède uniquement à la fin de la chaîne. Lorsqu’il est combiné avec ^ en début de motif, cela signifie que le motif doit correspondre à la chaîne entière, sans autres caractères avant ou après les M. (2) L’essence du module re est la fonction search, qui prend une expression régulière (pattern) et une chaîne ('M') qu’elle va tenter de faire correspondre. Si une correspondance est trouvée, search retourne un objet ayant diverses méthodes permettant de décrire la correspondance, sinon, search retourne None, la valeur nulle de Python. Tout ce qui nous intéresse à ce stade est de savoir si le motif est reconnu, ce que nous pouvons dire rien qu’en regardant la valeur retournée par search. 'M' est reconnu par cette expression régulière car le premier M optionnel correspond et que le second et troisième M sont ignorés. (3) 'MM' est reconnu puisque les premier et deuxième M optionnels correspondent et que le troisième est ignoré. (4) 'MMM' est reconnu puisque les trois M correspondent. (5) 'MMMM' n’est pas reconnu. Les trois M correspondent, mais l’expression régulière précise la fin de chaîne (par le caractère $) et la chaîne ne s’arrête pas là (à cause du quatrième M). Donc search retourne None. (6) Un élément intéressant est qu’une chaîne vide est reconnue par l’expression régulière, puisque tous les M sont optionnels. 7.3.2. Rechercher les centaines Les centaines présentent plus de difficultés que les milliers car elles peuvent être exprimées de plusieurs manières mutuellement exclusives, en fonction de leur valeur. • 100 = C • 200 = CC • 300 = CCC • 400 = CD • 500 = D • 600 = DC • 700 = DCC • 800 = DCCC • 900 = CM Il y a donc quatre motifs possibles : • CM • CD • 0 à 3 C (0 si les centaines valent 0) • D, suivi de 0 à 3 C Les deux derniers motifs peuvent être combinés en : • un D optionnel, suivi de 0 à 3 C L’exemple suivant montre comment valider les centaines en chiffres romains. Exemple 7.4. Rechercher les centaines >>> import re >>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' (1) >>> re.search(pattern, 'MCM') (2) >>> re.search(pattern, 'MD') (3) >>> re.search(pattern, 'MMMCCC') (4) >>> re.search(pattern, 'MCMC') (5) >>> re.search(pattern, '') (6) (1) Ce motif commence de la même manière que le précédent, en vérifiant le début de chaîne (^), puis les milliers (M?M?M?). Ensuite vient la nouvelle partie, entre parenthèses, qui définit un ensemble de trois motifs mutuellement exclusifs séparés par des barres verticales : CM, CD, and D?C? C?C? (qui est un D optionnel suivi de 0 à 3 C optionnels). Le processeur d’expressions régulières teste chacun de ces motifs dans l’ordre (de gauche à droite), prend le premier qui correspond et ignore le reste. (2) 'MCM' est reconnu car le premier M correspond, que le second et troisième M sont ignorés et que CM correspond (et donc les motifs CD et D?C?C?C? ne sont même pas examinés). MCM est la représentation de 1900. (3) 'MD' est reconnu car le premier M correspond, les deuxième et troisième M sont ignorés et que le motif D?C?C?C? reconnaît D (chacun des trois C est optionnel et est ignoré). MD est la représentation de 1500. (4) 'MMMCCC' est reconnu car les trois M correspondent et que le motif D?C?C?C? reconnaît CCC (le D est optionnel et est ignoré). MMMCCC est la représentation de 3300. (5) 'MCMC' n’est pas reconnu. Le premier M correspond, les deuxième et troisième M sont ignorés et le CM correspond, mais le $ ne correspond pas car nous ne sommes pas encore à la fin de la chaîne (il nous reste le caractère C à évaluer). Le C ne correspond pas comme partie du motif D?C?C? C? car le motif CM a déja été reconnu et qu’ils sont mutuellement exclusifs. (6) Fait intéressant, une chaîne vide est toujours reconnue par ce motif, car tous les M sont optionnels et sont ignorés et que la chaîne vide est reconnue par le motif D?C?C?C? dans lequel tous les caractères sont optionnels et sont ignorés. Ouf ! Vous voyez à quel point les expressions régulières peuvent devenir compliquées ? Et nous n’avons vu que les milliers et les centaines. Heureusement, si vous avez suivi jusque là, les dizaines sont relativement simples puisqu’elles suivent exactement le même motif. Mais continuons en examinant une autre manière d’exprimer ce motif. 7.4. Utilisation de la syntaxe {n,m} Dans la section précédente, nous avons vu un motif dans lequel le même caractère pouvait être répété jusqu’à trois fois. Il y a une autre manière d’exprimer cela dans les expressions régulière, que certaines personnes trouvent plus lisible. D’abord, revenons sur la méthode que nous avons utilisé dans l’exemple précédent. Exemple 7.5. L’ancienne méthode : chaque caractère est optionnel >>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M') (1) <_sre.SRE_Match object at 0x008EE090> >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'MM') (2) <_sre.SRE_Match object at 0x008EEB48> >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'MMM') (3) <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MMMM') (4) >>> (1) Cette chaîne est reconnue : le motif reconnaît le début de la chaîne, puis le premier M optionnel, mais pas de second ni de troisième M (ce qui est correct puisqu'ils sont optionnels), puis la fin de la chaîne. (2) Le motif reconnaît le début de la chaîne, puis le premier et le second M optionnels, mais pas de troisième M (ce qui est correct puisqu'il est optionnel), puis la fin de la chaîne. (3) Le motif reconnaît le début de la chaîne, puis les trois M optionnels, puis la fin de la chaîne. (4) Le motif reconnaît le début de la chaîne, puis les trois M optionnels, mais pas la fin de la chaîne (puisqu'il reste un M), la chaîne n'est donc pas reconnue et None est retourné. Exemple 7.6. La nouvelle méthode : de n à m >>> pattern = '^M{0,3}$' (1) >>> re.search(pattern, 'M') (2) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MM') (3) <_sre.SRE_Match object at 0x008EE090> >>> re.search(pattern, 'MMM') (4) <_sre.SRE_Match object at 0x008EEDA8> >>> re.search(pattern, 'MMMM') (5) >>> (1) Ce motifie signifie : <> Le 0 et le 3 peuvent être n’importe quel nombre, si nous voulons reconnaître au moins un, mais pas plus de trois caractères M, nous pouvons écrire M{1,3}. (2) Le motif reconnaît le début de la chaîne, puis un M sur trois possibles, puis la fin de la chaîne. (3) Le motif reconnaît le début de la chaîne, puis deux M sur trois possibles, puis la fin de la chaîne. (4) Le motif reconnaît le début de la chaîne, puis trois M sur trois possibles, puis la fin de la chaîne. (5) Le motif reconnaît le début de la chaîne, puis trois M sur trois possibles, puis ne reconnaît pas la fin de la chaîne. L’expression régulière permet jusqu'à trois caractères M avant la fin de la chaîne, mais il y en a quatre, donc la chaîne n'est pas reconnue et None est retourné. NOTE: Il n’y a aucun moyen de déterminer par un programme que deux expressions régulières sont équivalentes. Le mieux que vous puissiez faire est d’écrire de nombreux cas de test pour vérifier que leur comportements sont identiques pour les entrées pertinentes. Nous discuterons plus en détail l’écriture de cas de tests plus loin dans le livre. 7.4.1. Rechercher les dizaines et les unités Maintenant, nous allons étendre l’expression régulière pour prendre en compte les dizaines et les unités. L’exemple suivant montre la recherche des dizaines. Exemple 7.7. Rechercher les dizaines >>> pattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$' >>> re.search(pattern, 'MCMXL') (1) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCML') (2) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLX') (3) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXX') (4) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXXX') (5) >>> (1) Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis XL, puis la fin de la chaîne. Rappelez-vous que la syntaxe (A|B|C) signifie <>. XL est reconnu, donc XC et L?X?X?X? sont ignorés, puis la find de la chaîne est reconnue. MCML est la représentation en chiffres romains de 1940. (2) Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis L?X?X?X?. Pour L?X?X?X?, il reconnaît L et saute les trois caractères X optionnels. Il reconnaît ensuite la fin de la chaîne. MCML est la représentation en chiffres romains de 1950. (3) Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis le L optionnel et le premier X optionnel, saute les trois caractères X optionnels, puis reconnaît la fin de la chaîne. MCMLX est la représentation en chiffres romains de 1960. (4) Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis le L optionnel et les trois caractères X, puis la fin de la chaîne. MCMLXXX est la représentation en chiffres romains de 1980. (5) Le motif reconnaît le début de la chaîne, le premier M optionnel, puis CM, puis le L optionnel et les trois caractères X, puis ne reconnaît pas la fin de la chaîne puisqu'il y a encore un X non pris en charge. Donc la chaîne n'est pas reconnue et None est retourné. MCMLXXXX n'est pas un représentation en chiffres romains valide. L’expression pour les unités suit le même motif. Je vous épargne les détails et ne vous montre que le résultat final. >>> pattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$' A quoi est-ce que ça resemble en utilisant la syntaxe alternative avec {n,m} ? L’exemple suivant montre la nouvelle syntaxe. Exemple 7.8. Validation des chiffres romains avec {n,m} >>> pattern = '^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$' >>> re.search(pattern, 'MDLV') (1) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMDCLXVI') (2) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMMMDCCCLXXXVIII') (3) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'I') (4) <_sre.SRE_Match object at 0x008EEB48> (1) Le motif reconnaît le début de la chaîne, puis un sur un maximum de quatre caractères M, puis D?C{0,3}. Pour cette expression il reconnaît le D optionnel et zéro sur un maximum de trois caractères C. Ensuite il reconnaît L?X{0,3} avec le L optionnel et zéro sur un maximum de trois caractères X. Puis il reconnaît V?I{0,3} avec le V optionnel et zéro sur un maximum de trois caractères I, puis la fin de la chaîne. MDLV est la représentation en chiffres romains de 1555. (2) Le motif reconnaît le début de la chaîne, puis deux sur un maximum de quatre caractères M, puis D?C{0,3} avec un D et un sur un maximum de trois caractères C. Ensuite il reconnaît L?X{0,3} avec un L et un sur un maximum de trois caractères X. Puis il reconnaît V?I{0,3} avec un V and et un sur un maximum de trois caractères I, puis la fin de la chaîne. MMDCLXVI est la représentation en chiffres romains de 2666. (3) Le motif reconnaît le début de la chaîne, puis quatre sur un maximum de quatre caractères M, puis D?C{0,3} avec un D et trois sur un maximum de trois caractères C. Ensuite il reconnaît L?X{0,3} avec un L et trois sur un maximum de trois caractères X. Puis il reconnaît V?I{0,3} avec un V et trois sur un maximum de trois caractères I, puis la fin de la chaîne. MMMMDCCCLXXXVIII est la représentation en chiffres romains de 3888 et c'est le chiffre romain le plus long que vous pouvez écrire sans syntaxe étendue. (4) Regardez bien. Le motif reconnaît le début de la chaîne, puis zéro sur un maximum de quatre M, puis D?C{0,3} en sautant le D optionnel et zéro sur un maximum de trois C. Ensuite il reconnaît L?X{0,3} en sautant le L optionnel et zéro sur un maximum de trois X. Puis il reconnaît V?I{0,3} en sautant le V optionnel et un sur un maximum de trois I, puis la fin de la chaîne. Si vous avez suivi tout cela et que vous l'avez compris du premier coup, vous vous en sortez mieux que moi au début. Maintenant, imaginez devoir comprendre une expression régulière écrite par quelqu'un d'autre au mileu d'un fonction critique d'un programme de grande taille. Ou imaginez simplement de devoir revenir sur une de vos propres expressions régulières quelques mois plus tard. Je l'ai fait et ce n'est pas une partie de plaisir. Dans la prochaine section vous explorerez une syntaxe alternative qui rendra possible la maintenance de vos expressions régulières. 7.5. Expressions régulières détaillées Jusqu'à maintenant, vous n'avez vu que ce que j'appellerais des expressions régulières <>. Comme vous l'avez vu, elles sont difficiles à lire et même si vous comprenez ce qu'une d'entre elles fait, rien n'assure que vous pourrez la comprendre dans six mois. Ce qu'il faut, c'est une documentation intégrée. Python fournit pour cela les expressions régulières détaillées (verbose regular expressions). Une expression régulière détaillée diffère d'une expression régulière compacte de deux manières : • Les espaces sont ignorés. Les espaces, tabulations et retours chariot ne sont pas reconnus comme espaces, tabulations et retours chariot. Il ne sont pas reconnus du tout (si vous voulez reconnaître un espace dans une expression régulière détaillée, vous devez le faire précéder d'un caractère d'échapement <<\>>). • Les commentaires sont ignorés. Un commentaire dans une expression régulière détaillée est comme un commentaire dans du code Python : il commence par un caractère # et se poursuit jusqu'à la fin de la ligne. Dans le cas présent, c'est un commentaire à l'intérieur d'une chaîne de caractères multi-lignes plutôt que dans du code source, mais cela fonctionne de la même manière. Cela sera plus clair avec un exemple. Revenons à l'expression régulière compacte avec laquelle nous avons travaillé et transformons-la en une expression régulière détaillée. L'exemple suivant montre comment. Exemple 7.9. Expressions régulières intégrant des commentaires >>> pattern = """ ^ # beginning of string M{0,4} # thousands - 0 to 4 M's (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's), # or 500-800 (D, followed by 0 to 3 C's) (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's), # or 50-80 (L, followed by 0 to 3 X's) (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's), # or 5-8 (V, followed by 0 to 3 I's) $ # end of string """ >>> re.search(pattern, 'M', re.VERBOSE) (1) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE) (2) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'MMMMDCCCLXXXVIII', re.VERBOSE) (3) <_sre.SRE_Match object at 0x008EEB48> >>> re.search(pattern, 'M') (4) (1) La chose la plus importante à se rappeler lorsqu'on utilise des expressions régulières détaillées est qu'il faut passer un argument supplémentaire : re.VERBOSE est une constante définie dans le module re qui signale que le motif doit être traité comme une expresion régulière détaillée. Comme vous le voyez, ce motif comprend beaucoup d'espaces (qui sont tous ignorés) et plusieurs commentaires (qui sont tous ignorés). Une fois enlevés les espaces et les commentaires, on obtient exactement la même expression régulière que nous avons vu à la section précédente, mais elle est beaucoup plus lisible. (2) Le motif reconnaît le début de la chaîne, puis un sur un maximum de quatre M, puis CM, puis L et trois sur un maximum de trois X. Ensuite IX et la fin de la chaîne. (3) Le motif reconnaît le début de la chaîne, puis quatre sur un maximum de quatre M, puis D et trois sur un maximum de trois C. Ensuite L et trois sur un maximum de trois X, puis V et trois sur un maximum de trois I et la fin de la chaîne. (4) Rien n'est reconnu. Pourquoi ? Parce que le drapeau re.VERBOSE n'est pas mis et donc la fonction re.search traite le motif comme une expression régulière compacte, dans laquelle les espaces et les commentaires sont pris en compte. Python ne peut pas savoir si une expression régulière est détailée ou non. Python considère que chaque expression régulière est compacte, à moins que vous ne spécifiez qu'elle est détaillée. 7.6. Etude de cas : reconnaissance de numéros de téléphone Jusqu'ici nous nous sommes concentrés sur la reconnaissance de motifs complets, le motif est reconnu ou non. Mais les expressions régulières sont beaucoup plus puissantes que cela. Lorsqu'une expression régulière reconnaît un motif, nous pouvons sélectionner certaines parties du motif. Nous pouvons savoir ce qui a été reconnu et à quel endroit. Cet exemple est tiré d'un autre problème réel que j'ai eu au cours de mon travail précédent. Le problème : la reconnaissance de numéros de téléphone au Etats-Unis. Le client voulait que la saisie se fasse librement (dans un champ unique), mais voulait stocker le code régional, l'indicatif, le numéro et une extension optionnelle séparément dans la base de données. Je parcourais le Web et trouvais de nombreux exemples d'expressions régulières qui avaient pour but de faire cela, mais aucune n'était assez souple. Voici les numéros de téléphone qu'il fallait que j'accepte : • 800-555-1212 • 800 555 1212 • 800.555.1212 • (800) 555-1212 • 1-800-555-1212 • 800-555-1212-1234 • 800-555-1212x1234 • 800-555-1212 ext. 1234 • work 1-(800) 555.1212 #1234 Quelle diversité ! Dans chacun de ces cas, je devais savoir que le code régional était 800, l'indicatif 555 et le reste du numéro 1212. Pour les numéros avec extension, je devais savoir que celle-ci était 1234. Nous allons développer une solution pour la reconnaissance des numéros de téléphone. Cet exemple montre la première étape. Exemple 7.10. Trouver des numéros >>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$') (1) >>> phonePattern.search('800-555-1212').groups() (2) ('800', '555', '1212') >>> phonePattern.search('800-555-1212-1234') (3) >>> (1) Lisez toujours les expressions régulières de la gauche vers la droite. Celle-ci reconnaît le début de la chaîne, puis (\d{3}). Que veut dire ce \d {3}? Le {3} signifie <>, c'est une variante de la syntaxe {n,m} que nous avons vu plus haut. \d signifie <> (de 0 à 9). En le mettant entre parenthèses, nous disons <>. Ensuite, le motif reconnaît un tiret, puis un autre groupe de trois chiffres, un autre tiret, un groupe de quatre chiffres et la fin de la chaîne. (2) Pour accéder aux groupes que l'expression régulière à identifiés, utilisez la méthode groups() de l'objet que la fonction search retourne. Elle retournera un tuple du nombre de groupes définis dans l'expression régulières. Dans ce cas, nous avons défini trois groupes, deux de trois chiffres et un de quatre. (3) Cette expression régulière n'est pas la réponse finale car elle ne prend pas en compte les numéros de téléphone avec une extension à la fin. Pour cela, nous allons devoir la modifier. Exemple 7.11. Trouver l'extension >>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$') (1) >>> phonePattern.search('800-555-1212-1234').groups() (2) ('800', '555', '1212', '1234') >>> phonePattern.search('800 555 1212 1234') (3) >>> >>> phonePattern.search('800-555-1212') (4) >>> (1) Cette expression régulière est pratiquement identique à la précédente. Elle reconnaît le début de la chaîne, puis un groupe identifié de trois chiffres, puis un tiret, puis un groupe identifié de trois chiffres, puis un tiret, puis un groupe identifié de quatre chiffres. Ce qui est nouveau, c'est qu'elle reconnaît ensuite un autre tiret puis un groupe identifié de un chiffre ou plus, puis la fin de la chaîne. (2) La méthode groups() retourne maintenant un tuple de quatre éléments, puisque l'expression régulière définit quatre groupe à identifier. (3) Malheureusement cette expression régulière n'est pas la réponse finale non plus, puisqu'elle considère que les différentes parties du numéro de téléphone sont séparées par des tirets. Et si elles étaient séparées par des espaces, des virgules ou des points ? Il nous faut une solution plus générale pour identifier différents types de séparateurs. (4) Non seulement cette expression régulière ne fait pas tout ce que nous voulions, elle est en fait un pas en arrière puisqu'elle ne peut pas reconnaître de numéros de téléphone sans extension. Ce n'est pas du tout ce que nous voulions, si l'extension est présente, nous voulons la connaître, mais si elle ne l'est pas, nous voulons tout de même connaître les différentes parties du numéro. L'exemple suivant montre l'expression régulière qui reconnaît les séparateurs entre les différentes parties d'un numéro de téléphone. Exemple 7.12. Reconnaissance des séparateurs >>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$') (1) >>> phonePattern.search('800 555 1212 1234').groups() (2) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212-1234').groups() (3) ('800', '555', '1212', '1234') >>> phonePattern.search('80055512121234') (4) >>> >>> phonePattern.search('800-555-1212') (5) >>> (1) Faites bien attention. Nous reconnaissons le début de la chaîne, puis un groupe de trois chiffres, puis \D+. Qu'est-ce que c'est que ça ? Et bien, \ D reconnaît n'importe quel caractère sauf un chiffre et + signifie <>. Donc \D+ reconnaît un ou plusieurs caractères n'étant pas des chiffres. C'est ce que nous utilisons pour essayer de reconnaitre les séparateurs. (2) Utiliser \D+ au lieu de - nous permet de reconnaître des numéros de téléphone dont les différentes parties sont séparées par des espaces au lieu de tirets. (3) Bien sûr, les numéro de téléphone séparés par des tirets sont toujours reconnus. (4) Malheureusement ce n'est pas encore la réponse définitive car elle suppose qu'il y a bien un séparateur. Et si le numéro est saisi sans espaces ni tirets ? (4) Le problème de l'extension optionnelle n'a toujours pas été réglé. Maintenant nous avons deux problèmes, mais nous pouvons les régler tous les deux grâce à la même technique. L'exemple suivant montre l'expression régulière qui reconnaît les numéros de téléphone sans séparateurs. Exemple 7.13. Reconnaissance des numéros de téléphone sans séparateurs >>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') (1) >>> phonePattern.search('80055512121234').groups() (2) ('800', '555', '1212', '1234') >>> phonePattern.search('800.555.1212 x1234').groups() (3) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() (4) ('800', '555', '1212', '') >>> phonePattern.search('(800)5551212 x1234') (5) >>> (1) La seule modification depuis la dernière étape est de remplacer le + par un *. Au lieu de \D+ entre les différentes parties du numéro de téléphone, nous avons maintenant \D*. Vous vous rappelez que + signifie <> et bien * signifie <>. Nous devrions donc pouvoir reconnaître des numéros de téléphone qui n'ont pas de séparateur du tout. (2) Et ça marche. Nous avons reconnu le début de la chaîne, puis un groupe identifié de trois chiffres (800), puis zéro caractères non numériques, puis un groupe identifié de trois caractères (555), puis zéro caractères non numériques, puis un groupe identifié de quatre caractères (1212), puis zéro caractères non numériques, puis un groupe identifié d'un nombre quelconque de caractères (1234), puis la fin de la chaîne. (3) D'autre variantes marchent également maintenant : des points à la place des tirets et un espace et un x avant l'extension. (4) Finalement, nous avons trouvé une solution à notre problème, les extension sont vraiment optionnelles. Si aucune extension n'est trouvée la méthode groups() retourne un tuple de quatre éléments, mais le quatrième élément est simplement une chaîne vide. (5) La mauvaise nouvelle, c'est que nous n'avons pas terminé. Il y a un caractère supplémentaire avant le code régional, mais l'expression régulière suppose que le code régional est la première chose au début de la chaîne. Bien sûr, nous pouvons utiliser la même technique <> pour sauter les caractères situés avant le code régional. L'exemple suivant montre comment prendre en compte les caractères au début des numéros de téléphone. Exemple 7.14. Reconnaissance des caractères de début >>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') (1) >>> phonePattern.search('(800)5551212 ext. 1234').groups() (2) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212').groups() (3) ('800', '555', '1212', '') >>> phonePattern.search('work 1-(800) 555.1212 #1234') (4) >>> (1) C'est la même chose que dans l'exemple précédent, sauf que nous reconnaissons \D*, c'est à dire zéro ou plus caractères non numériques, avant le premier groupe identifié (le code régional). Notez que nous n'identifions pas ces caractères non numériques (ils ne sont pas entre parenthèses). Si ils sont présents, nous les passons simplement et commençont à identifier le code régional quand nous y arrivons. (2) Le numéro de téléphone est correctement reconnu, même avec la parenthèse ouvrante devant le code régional (la parenthèse fermante était déjà prise en compte, elle est considérée comme un séparateur non-numérique et reconnue par le \D* suivant le premier groupe identifié). (3) Une simple vérification de cohérence pour nous assurer que nous n'avons rien endommagé de ce qui fonctionnait déjà. Puisque les caractères de début sont entièrement optionnels, le début de la chaîne est reconnu, puis zéro caractères non-numériques, puis un groupe identifié de trois caractères (800), puis un caractère non-numérique (le tiret), puis un groupe identifié de trois caractères (555), puis un caractère non-numérique (le tiret), puis un groupe identifié de quatre caractères (1212), puis zéro caractères non-numériques, puis un groupe identifié de zéro caractères numériques, puis la fin de la chaîne. (4) Mais il y a encore un problème. Pourquoi ce numéro ne fonctionne-t-il pas ? Parce qu'il y a un 1 avant le code régional et que nous avons considéré que tous les caractères de début sont non-numériques (\D*). Faisons le point. Jusqu'à présent, nos expressions régulières commençaient toujours la reconnaissance en début de chaîne, mais maintenant nous voyons qu'il peut y avoir un nombre indeterminé de caractères à ignorer en début de chaîne. Au lieu d'essayer de les identifier pour les sauter, essayons une approche différente : ne pas reconnaître explicitemment le début de la chaîne. C'est ce que nous verrons dans l'exemple suivant. Exemple 7.15. Un numéro de téléphone, où qu'il soit >>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$') (1) >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() (2) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212') (3) ('800', '555', '1212', '') >>> phonePattern.search('80055512121234') (4) ('800', '555', '1212', '1234') (1) Notez l'absence de ^ dans cette expression régulière. Nous ne reconnaissons plus le début de la chaîne. Rien ne nous oblige à reconnaître la chaîne entière, le moteur d'expressions régulières se débrouillera pour trouver où commence la reconnaissance. (2) Maintenant, nous pouvons reconnaître un numéro de téléphone précédé de caractères et de chiffres et segmenté par des séparateurs de tout type et de toute taille. (3) Contrôle de cohérence, ça fonctionne toujours. (4) Cela aussi fonctionne toujours. Vous avez pu voir comment les expressions régulières peuvent rapidemment échapper à tout contrôle. Parcourez les différentes versions de notre expression régulière, pouvez-vous dire quelles différences les séparent ? Tant que nous comprenons encore la version finale (et c'est bien la version finale, si vous découvrez un cas qu'elle n'est pas capable de traiter, je ne veux pas en entendre parler), écrivons-la sous la forme d'une expression régulière détaillée avant d'oublier les choix que nous avons fait. Exemple 7.16. Reconnaissance des numéros de téléphone (version finale) >>> phonePattern = re.compile(r''' # don't match beginning of string, number can start anywhere (\d{3}) # area code is 3 digits (e.g. '800') \D* # optional separator is any number of non-digits (\d{3}) # trunk is 3 digits (e.g. '555') \D* # optional separator (\d{4}) # rest of number is 4 digits (e.g. '1212') \D* # optional separator (\d*) # extension is optional and can be any number of digits $ # end of string ''', re.VERBOSE) >>> phonePattern.search('work 1-(800) 555.1212 #1234').groups() (1) ('800', '555', '1212', '1234') >>> phonePattern.search('800-555-1212') (2) ('800', '555', '1212', '') (1) Mis à part le fait qu'elle est distribuée sur plusieurs lignes, c'est exactement la même expression régulière qu'à la dernière étape, il n'est donc pas étonnant qu'elle reconnaisse les mêmes entrées de manière identique. (2) Vérification de cohérence finale. Oui, ça marche toujours, nous avons terminé. Pour en savoir plus sur les expressions régulières • La Regular Expression HOWTO (http://py-howto.sourceforge.net/regex/ regex.html) explique les expressions régulières et leur usage en Python. • La Python Library Reference (http://www.python.org/doc/current/lib/) résume le module re (http://www.python.org/doc/current/lib/module-re.html). 7.7. Résumé Nous n'avons vu que la pointe de la partie émergée de l'iceberg des possibilités offertes par les expressions régulières. En d'autres termes, même si vous vous sentez totalement dépassé, vous n'avez encore rien vu. Vous devez maintenant être familiarisé avec les techniques suivantes : • ^ reconnaît le début d'une chaîne. • $ reconnaît la fin d'une chaîne. • \b reconnaît la limite d'un mot. • \d reconnaît un chiffre. • \D reconnaît un caractère non-numérique. • x? reconnaît un caractère x optionnel (autrement dit, il reconnaît un x zéro ou une fois). • x* reconnaît zéro ou plus x. • x+ reconnaît un ou plusieurs x. • x{n,m} reconnaît un caractère x au moins n fois, mais pas plus de m fois. • (a|b|c) reconnaît soit a soit b soit c. • (x) en général est un groupe identifié. Vous pouvez obtenir la valeur de ce qui a été reconnu à l'aide de la méthode groups() de l'objet retourné par re.search. Les expressions régulières sont extrêmement puissantes, mais elles ne sont pas la solution correcte pour chaque problème. Vous devriez en apprendre assez sur elles pour savoir quand elles sont appropriées, quand elle peuvent résoudre votre problème et quand elles causent plus de problèmes qu'elles n'en résolvent. Certaines personnes, face à un problème, se disent <> Maintenant elles ont deux problèmes. --Jamie Zawinski, dans comp.emacs.xemacs (http://groups.google.com/ groups?selm=33F0C496.370D7C45%40netscape.com) ━━━━━━━━━━━━━━ ^[2] Note du traducteur : Je suppose que cet exemple suffit pour les lecteurs francophones. Chapitre 8. Traitement du HTML 8.1. Plonger Je vois souvent sur comp.lang.python (http://groups.google.com/groups?group= comp.lang.python) des questions comme <> <> <> Ce chapitre répondra à toutes ces questions. Voici un programme Python complet et fonctionnel en deux parties. La première partie, BaseHTMLProcessor.py, est un outil générique destiné à vous aider à traiter des fichiers HTML en parcourant les balises et les blocs de texte. La deuxième partie, dialect.py, est un exemple montrant comment utiliser BaseHTMLProcessor.py pour traduire le texte d’un document HTML sans toucher aux balises. Lisez les doc strings et les commentaires pour avoir une vue d’ensemble de ce qui se passe. Une grande partie va avoir l’air magique parce qu’il n’est pas évident de voir comment ces méthodes de classes sont appelées. Ne vous inquiétez pas, tout vous sera bientôt expliqué. Exemple 8.1. BaseHTMLProcessor.py Si vous ne l’avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples (http://diveintopython.org/download/ diveintopython-examples-5.4.zip) du livre. from sgmllib import SGMLParser import htmlentitydefs class BaseHTMLProcessor(SGMLParser): def reset(self): # extend (called by SGMLParser.__init__) self.pieces = [] SGMLParser.reset(self) def unknown_starttag(self, tag, attrs): # called for each start tag # attrs is a list of (attr, value) tuples # e.g. for
, tag="pre", attrs=[("class", "screen")]
        # Ideally we would like to reconstruct original tag and attributes, but
        # we may end up quoting attribute values that weren't quoted in the source
        # document, or we may change the type of quotes around the attribute value
        # (single to double quotes).
        # Note that improperly embedded non-HTML code (like client-side Javascript)
        # may be parsed incorrectly by the ancestor, causing runtime script errors.
        # All non-HTML code must be enclosed in HTML comment tags ()
        # to ensure that it will pass through this parser unaltered (in handle_comment).
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):
        # called for each end tag, e.g. for 
, tag will be "pre" # Reconstruct the original end tag. self.pieces.append("" % locals()) def handle_charref(self, ref): # called for each character reference, e.g. for " ", ref will be "160" # Reconstruct the original character reference. self.pieces.append("&#%(ref)s;" % locals()) def handle_entityref(self, ref): # called for each entity reference, e.g. for "©", ref will be "copy" # Reconstruct the original entity reference. self.pieces.append("&%(ref)s" % locals()) # standard HTML entities are closed with a semicolon; other entities are not if htmlentitydefs.entitydefs.has_key(ref): self.pieces.append(";") def handle_data(self, text): # called for each block of plain text, i.e. outside of any tag and # not containing any character or entity references # Store the original text verbatim. self.pieces.append(text) def handle_comment(self, text): # called for each HTML comment, e.g. # Reconstruct the original comment. # It is especially important that the source document enclose client-side # code (like Javascript) within comments so it can pass through this # processor undisturbed; see comments in unknown_starttag for details. self.pieces.append("" % locals()) def handle_pi(self, text): # called for each processing instruction, e.g. # Reconstruct original processing instruction. self.pieces.append("" % locals()) def handle_decl(self, text): # called for the DOCTYPE, if present, e.g. # # Reconstruct original DOCTYPE self.pieces.append("" % locals()) def output(self): """Return processed HTML as a single string""" return "".join(self.pieces) Exemple 8.2. dialect.py import re from BaseHTMLProcessor import BaseHTMLProcessor class Dialectizer(BaseHTMLProcessor): subs = () def reset(self): # extend (called from __init__ in ancestor) # Reset all data attributes self.verbatim = 0 BaseHTMLProcessor.reset(self) def start_pre(self, attrs): # called for every
 tag in HTML source
        # Increment verbatim mode count, then handle tag like normal
        self.verbatim += 1
        self.unknown_starttag("pre", attrs)

    def end_pre(self):
        # called for every 
tag in HTML source # Decrement verbatim mode count self.unknown_endtag("pre") self.verbatim -= 1 def handle_data(self, text): # override # called for every block of text in HTML source # If in verbatim mode, save text unaltered; # otherwise process the text with a series of substitutions self.pieces.append(self.verbatim and text or self.process(text)) def process(self, text): # called from handle_data # Process text block by performing series of regular expression # substitutions (actual substitions are defined in descendant) for fromPattern, toPattern in self.subs: text = re.sub(fromPattern, toPattern, text) return text class ChefDialectizer(Dialectizer): """convert HTML to Swedish Chef-speak based on the classic chef.x, copyright (c) 1992, 1993 John Hagerman """ subs = ((r'a([nu])', r'u\1'), (r'A([nu])', r'U\1'), (r'a\B', r'e'), (r'A\B', r'E'), (r'en\b', r'ee'), (r'\Bew', r'oo'), (r'\Be\b', r'e-a'), (r'\be', r'i'), (r'\bE', r'I'), (r'\Bf', r'ff'), (r'\Bir', r'ur'), (r'(\w*?)i(\w*?)$', r'\1ee\2'), (r'\bow', r'oo'), (r'\bo', r'oo'), (r'\bO', r'Oo'), (r'the', r'zee'), (r'The', r'Zee'), (r'th\b', r't'), (r'\Btion', r'shun'), (r'\Bu', r'oo'), (r'\BU', r'Oo'), (r'v', r'f'), (r'V', r'F'), (r'w', r'w'), (r'W', r'W'), (r'([a-z])[.]', r'\1. Bork Bork Bork!')) class FuddDialectizer(Dialectizer): """convert HTML to Elmer Fudd-speak""" subs = ((r'[rl]', r'w'), (r'qu', r'qw'), (r'th\b', r'f'), (r'th', r'd'), (r'n[.]', r'n, uh-hah-hah-hah.')) class OldeDialectizer(Dialectizer): """convert HTML to mock Middle English""" subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'), (r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'), (r'ick\b', r'yk'), (r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'), (r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'), (r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'), (r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'), (r'([aeiou])re\b', r'\1r'), (r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'), (r'tion\b', r'cioun'), (r'ion\b', r'ioun'), (r'aid', r'ayde'), (r'ai', r'ey'), (r'ay\b', r'y'), (r'ay', r'ey'), (r'ant', r'aunt'), (r'ea', r'ee'), (r'oa', r'oo'), (r'ue', r'e'), (r'oe', r'o'), (r'ou', r'ow'), (r'ow', r'ou'), (r'\bhe', r'hi'), (r've\b', r'veth'), (r'se\b', r'e'), (r"'s\b", r'es'), (r'ic\b', r'ick'), (r'ics\b', r'icc'), (r'ical\b', r'ick'), (r'tle\b', r'til'), (r'll\b', r'l'), (r'ould\b', r'olde'), (r'own\b', r'oune'), (r'un\b', r'onne'), (r'rry\b', r'rye'), (r'est\b', r'este'), (r'pt\b', r'pte'), (r'th\b', r'the'), (r'ch\b', r'che'), (r'ss\b', r'sse'), (r'([wybdp])\b', r'\1e'), (r'([rnt])\b', r'\1\1e'), (r'from', r'fro'), (r'when', r'whan')) def translate(url, dialectName="chef"): """fetch URL and translate using dialect dialect in ("chef", "fudd", "olde")""" import urllib sock = urllib.urlopen(url) htmlSource = sock.read() sock.close() parserName = "%sDialectizer" % dialectName.capitalize() parserClass = globals()[parserName] parser = parserClass() parser.feed(htmlSource) parser.close() return parser.output() def test(url): """test all dialects against URL""" for dialect in ("chef", "fudd", "olde"): outfile = "%s.html" % dialect fsock = open(outfile, "wb") fsock.write(translate(url, dialect)) fsock.close() import webbrowser webbrowser.open_new(outfile) if __name__ == "__main__": test("http://diveintopython.org/odbchelper_list.html") Exemple 8.3. Sortie de dialect.py Ce script effectue la traduction de la Section 3.2, «Présentation des listes» en pseudo-Chef Suédois (http://diveintopython.org/chef.html) (des Muppets), pseudo-Elmer Fudd (http://diveintopython.org/fudd.html) (de Bugs Bunny) et en pseudo-ancien Anglais (http://diveintopython.org/olde.html) (librement adapté de The Canterbury Tales de Chaucer). Si vous regardez le source HTML de la sortie, vous verrez que toutes les balises HTML et les attributs sont intacts mais que le texte entre les balises a été <> dans le pseudo-langage. Si vous regardez plus attentivement, vous verrez qu’en fait, seuls les titres et les paragraphes ont été traduits, les listing de code et les exemples d’écrans ont été laissé intacts.

Lists awe Pydon's wowkhowse datatype. If youw onwy expewience wif wists is awways in Visuaw Basic ow (God fowbid) de datastowe in Powewbuiwdew, bwace youwsewf fow Pydon wists.

8.2. Présentation de sgmllib.py Le traitement du HTML est divisé en trois étapes : diviser le HTML en éléments, modifier les éléments et reconstruire le HTML à partir des éléments. La première étape est réalisée par sgmllib.py, qui fait partie de la bibliothèque standard de Python. La clé de la compréhension de ce chapitre est de réaliser que le HTML n’est pas seulement du texte, c’est du texte structuré. La structure est dérivée de la séquence plus ou moins hiérarchique de balises de début et de fin. Habituellement, on ne travaille pas de cette manière sur du HTML, on travaille textuellement dans un éditeur de texte ou visuellement dans un navigateur ou un éditeur de pages web. sgmllib.py présente le HTML de manière structurelle. sgmllib.py contient une classe principale : SGMLParser. SGMLParser analyse le HTML et le décompose en éléments utiles, comme des balises de début et de fin. Dès qu’il parvient à extraire des données un élément utile, il appelle une de ses propres méthodes en fonction de l’élément trouvé. Pour utiliser l’analyseur, on dérive une classe de SGMLParser et on redéfinit ces méthodes. C’est ce que j’entendais par présentation du HTML de manière structurelle : la structure du code HTML détermine la séquence d’appels de méthodes et les arguments passés à chaque méthode. SGMLParser décompose le HTML en 8 sortes de données et appelle une méthode différente pour chacune d’entre elles : Balise de début Une balise HTML qui ouvre un bloc, comme , , ou
,
    ou une balise autonome comme 
ou . Lorsqu’il trouve une balise de début tagname, SGMLParser cherche une méthode nommée start_tagname ou do_tagname. Par exemple, lorsqu’il trouve une balise
, il cherche une
    méthode nommée start_pre ou do_pre. S’il la trouve, SGMLParser l’appelle
    avec une liste des attributs de la balise en paramètre, sinon il appelle
    unknown_starttag avec le nom de la balise et la liste de ses attributs en
    paramètre.
Balise de fin
    Une balise HTML qui ferme un bloc, comme , ,  ou . Lorsqu’il trouve une balise de fin, SGMLParser cherche une méthode
    nommée end_tagname. S’il la trouve, SGMLParser appelle cette méthode, sinon
    il appelle unknown_endtag avec le nom de la balise.
Référence de caractère
    Un caractère référencé par son équivalent décimal ou hexadécimal, comme &#
    160;. Lorsqu’il en trouve une, SGMLParser appelle handle_charref avec le
    texte de l’équivalent décimal ou hexadécimal.
Référence d’entité
    Une entité HTML, comme ©. Lorsqu’il en trouve une, SGMLParser appelle
    handle_entityref avec le nom de l’entité HTML.
Commentaire
    Un commentaire HTML, encadré par . Lorsqu’il en trouve un,
    SGMLParser appelle handle_comment avec le corps du commentaire.
Instruction de traitement
    Une instruction de traitement HTML, encadrée par . Lorsqu’il en
    trouve une, SGMLParser appelle handle_pi avec le corps de l’instruction.
Déclaration
    Une déclaration HTML, comme un DOCTYPE, encadrée par . Lorsqu’il en
    trouve une, SGMLParser appelle handle_decl avec le corps de la déclaration
Données texte
    Un bloc de texte. Tout ce qui n’entre dans aucune des 7 catégories
    précédentes. Lorsqu’il en trouve un, SGMLParser appelle handle_data avec le
    texte.

    Important: Evolution du langage : DOCTYPE
    Python 2.0 avait un bogue qui empêchait SGMLParser de reconnaître les
    déclarations (handle_decl n’était jamais appelé), ce qui veut dire que les
    DOCTYPEs étaient ignorés silencieusement. Ce bogue est corrigé dans Python
    2.1.

sgmllib.py est accompagné d'une suite de tests pour illustrer cela. Si on
exécute sgmllib.py en lui passant le nom d’un fichier HTML en argument de ligne
de commande, il affichera les balises et les autres éléments au fur et à mesure
qu’il analyse le fichier. Il fait cela en dérivant une classe de SGMLParser et
en définissant des méthodes comme unknown_starttag, unknown_endtag, handle_data
et autres qui ne font qu’afficher leur argument.

    ASTUCE: Spécification d’arguments de ligne de commande sous Windows
    Dans l’IDE ActivePython sous Windows, vous pouvez spécifier des arguments
    de ligne de commande dans la boîte de dialogue <>. Séparez les
    différents arguments par des espaces.

Exemple 8.4. Exemple de test de sgmllib.py

Voici un extrait de la table des matières de la version HTML de ce livre. Bien
sûr, les chemins peuvent être différents. Si vous n'avez pas téléchargé la
version HTML du livre, vous pouvez le faire à l'adresse suivante : http://
diveintopython.org/.

c:\python23\lib> type "c:\downloads\diveintopython\html\toc\index.html"



   
      

      Dive Into Python
      

... nous coupons la suite pour rester bref ...

En l’utilisant avec la suite de tests de sgmllib.py, on obtient la sortie
suivante :

c:\python23\lib> python sgmllib.py "c:\downloads\diveintopython\html\toc\index.html"
data: '\n\n'
start tag: 
data: '\n   '
start tag: 
data: '\n      '
start tag: 
data: '\n   \n      '
start tag: 
data: 'Dive Into Python'
end tag: 
data: '\n      '
start tag: 
data: '\n      '

... nous coupons la suite pour rester bref ...

Voici le plan du reste de ce chapitre :

  • Dérivation de SGMLParser pour créer des classes qui extraient des données
    intéressantes de documents HTML.
  • Dérivation de SGMLParser pour créer BaseHTMLProcessor, qui redéfinit les 8
    méthodes de gestion et les utilise pour reconstruire le code HTML à partir
    de ses éléments.
  • Dérivation de BaseHTMLProcessor pour créer Dialectizer, qui ajoute des
    méthodes traitant des balises HTML spécifiques et redéfinit la méthode
    handle_data pour fournir une structure de traitement de blocs de tests
    entre les balises HTML.
  • Dérivation de Dialectizer pour créer des classes qui définissent des règles
    de traitement du texte utilisées par Dialectizer.handle_data.
  • Ecriture d’une suite de tests qui récupère une véritable page web de http:/
    /diveintopython.org/ et en traite le contenu.

En cours de route, vous apprendrez également locals, globals et le formatage de
chaînes à l'aide de dictionnaire.

8.3. Extraction de données de documents HTML

Pour extraire des données de documents HTML, on dérive une classe de SGMLParser
et on définit des méthodes pour chaque balise ou entité que l’on souhaite
traiter.

La première étape pour extraire des données d’un document HTML est d’obtenir le
HTML. Si vous avez un fichier HTML, vous pouvez le lire à l’aide des fonctions
de fichier, mais le plus intéressant est d’obtenir le HTML depuis des sites
web.

Exemple 8.5. Présentation de urllib

>>> import urllib                                       (1)
>>> sock = urllib.urlopen("http://diveintopython.org/") (2)
>>> htmlSource = sock.read()                            (3)
>>> sock.close()                                        (4)
>>> print htmlSource                                    (5)

      
   Dive Into Python








[...coupé...]

(1) Le module urllib fait partie de la bibliothèque standard de Python. Il
    comprend des fonctions permettant d’obtenir des information et des données
    à partir d’URLs (principalement des pages web).
(2) L’usage le plus simple de urllib est de lire le texte complet d’une page
    web à l’aide de la fonction urlopen. L’ouverture d’une URL est semblable à
    l’ouverture d’un fichier. La valeur de retour de urlopen est un objet
    semblable à un objet-fichier et dont certaines méthodes sont les mêmes que
    celles d’un objet-fichier.
(3) La chose la plus simple à faire avec l’objet retourné par urlopen est
    d’appeler read, qui lit l’ensemble du code HTML de la page web en une
    chaîne unique. L’objet permet également l’emploi de readlines, qui lit le
    code ligne par ligne et le stocke dans une liste.
(4) Quand vous n’en avez plus besoin, assurez vous de fermer l’objet par un
    close, comme pour un objet fichier.
(5) Nous avons maintenant l’ensemble du code HTML de la page d’accueil de http:
    //diveintopython.org/ dans une chaîne et nous sommes prêts à l’analyser.

Exemple 8.6. Présentation de urllister.py

Si vous ne l’avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que
les autres exemples (http://diveintopython.org/download/
diveintopython-examples-5.4.zip) du livre.

from sgmllib import SGMLParser

class URLLister(SGMLParser):
    def reset(self):                              (1)
        SGMLParser.reset(self)
        self.urls = []

    def start_a(self, attrs):                     (2)
        href = [v for k, v in attrs if k=='href'] (3) (4)
        if href:
            self.urls.extend(href)

(1) reset est appelé par la méthode __init__ de SGMLParser et peut également
    être appelé manuellement quand une instance de l’analyseur a été créée.
    Donc, si vous avez une quelconque initialisation à faire, faites la dans
    reset et pas dans __init__, de manière à ce que la réinitialisation se
    fasse correctement lorsque quelqu’un réutilise une instance de l’analyseur.
(2) start_a est appelé par SGMLParser à chaque fois qu’il trouve une balise
    . La balise peut contenir un attribut href et/ou d’autres attributs
    comme name ou title. Le paramètre attrs est une liste de tuples,
    [(attribut, valeur), (attribut, valeur), ...]. La balise peut aussi être un
    simple , ce qui est une balise HTML valide (bien qu’inutile), dans ce
    cas attrs sera une liste vide.
(3) Nous pouvons savoir si la balise  a un attribut href à l’aide d’une
    simple mutation de liste multi-variable.
(4) Les comparaisons de chaînes comme k=='href' sont toujours sensibles à la
    casse, mais ça ne pose pas de problème ici car SGMLParser convertit les
    noms d’attributs en minuscules lors de la création de attrs.

Exemple 8.7. Utilisation de urllister.py

>>> import urllib, urllister
>>> usock = urllib.urlopen("http://diveintopython.org/")
>>> parser = urllister.URLLister()
>>> parser.feed(usock.read())         (1)
>>> usock.close()                     (2)
>>> parser.close()                    (3)
>>> for url in parser.urls: print url (4)
toc/index.html
#download
#languages
toc/index.html
appendix/history.html
download/diveintopython-html-5.0.zip
download/diveintopython-pdf-5.0.zip
download/diveintopython-word-5.0.zip
download/diveintopython-text-5.0.zip
download/diveintopython-html-flat-5.0.zip
download/diveintopython-xml-5.0.zip
download/diveintopython-common-5.0.zip


... nous coupons la suite pour rester bref ...

(1) Appelez la méthode feed, définie dans SGMLParser, pour charger le code HTML
    dans l’analyseur.^[3] La méthode prend une chaîne en argument, ce qui est
    ce que usock.read() retourne.
(2) Comme pour les fichiers, vous devez fermer vos objets URL par close dès que
    vous n’en avez plus besoin.
(3) Vous devez également fermer l’objet analyseur par close, mais pour une
    raison différente. La méthode feed ne garantit pas qu’elle traite tout le
    code HTML que vous lui passez, elle peut la garder dans un tampon en
    attendant que vous lui en passiez plus. Quand il n’y en a plus, appelez
    close pour vider le tampon et forcer l’analyse de tout le code.
(4) Une fois le parser fermé par close, l’analyse est complète et parser.urls
    contient une liste de toutes les URLs pour lesquelles il y a un lien dans
    le document HTML.

8.4. Présentation de BaseHTMLProcessor.py

SGMLParser ne produit rien de lui même. Il ne fait qu’analyser et appeler une
méthode pour chaque élément intéressant qu’il trouve, mais les méthodes ne font
rien. SGMLParser est un consommateur de HTML : il prend du code HTML et le
décompose en petits éléments structurés. Comme nous l’avons vu dans la section
précédente, on peut dériver SGMLParser pour définir une classe qui trouve des
balises spécifiques et produit quelque chose d’utile, comme une liste de tous
les liens d’une page web. Nous allons maintenant aller un peu plus loin en
définissant une classe qui prends tout ce que SGMLParser lui envoi et
reconstruit entièrement le document HTML. En termes techniques, cette classe
sera un producteur de HTML.

BaseHTMLProcessor est dérivé de SGMLParser et fournit les 8 méthodes de prise
en charge essentielles : unknown_starttag, unknown_endtag, handle_charref,
handle_entityref, handle_comment, handle_pi, handle_decl et handle_data.

Exemple 8.8. Présentation de BaseHTMLProcessor

class BaseHTMLProcessor(SGMLParser):
    def reset(self):                        (1)
        self.pieces = []
        SGMLParser.reset(self)

    def unknown_starttag(self, tag, attrs): (2)
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

    def unknown_endtag(self, tag):          (3)
        self.pieces.append("" % locals())

    def handle_charref(self, ref):          (4)
        self.pieces.append("&#%(ref)s;" % locals())

    def handle_entityref(self, ref):        (5)
        self.pieces.append("&%(ref)s" % locals())
        if htmlentitydefs.entitydefs.has_key(ref):
            self.pieces.append(";")

    def handle_data(self, text):            (6)
        self.pieces.append(text)

    def handle_comment(self, text):         (7)
        self.pieces.append("" % locals())

    def handle_pi(self, text):              (8)
        self.pieces.append("" % locals())

    def handle_decl(self, text):
        self.pieces.append("" % locals())

(1) reset est appelé par SGMLParser.__init__ initialise self.pieces en une
    liste vide avant d’appeler la méthode ancêtre. self.pieces est une donnée
    attribut qui contient les éléments du document HTML que nous assemblons.
    Chaque méthode de prise en charge va reconstruire le code HTML que
    SGMLParser a analysé et chaque méthode ajoutera la chaîne résultante à
    self.pieces. Notez que self.pieces est une liste. Vous pouvez être tenté de
    la définir comme une chaîne et de lui ajouter simplement chaque élément.
    Cela fonctionnerait mais Python gère les listes de manière bien plus
    efficiente.^[4]
(2) Comme BaseHTMLProcessor ne définit aucune méthode pour des balises
    spécifiques (comme la méthode start_a de urllister.py), SGMLParser appelera
    unknown_starttag pour chaque balise de début. Cette méthode prend en
    paramètre la balise (tag) et la liste des paires nom/valeurs de ses
    attributs (attrs), reconstruit le code HTML originel et l’ajoute à
    self.pieces. Le formatage de chaîne ici est un peu étrange, nous
    l’expliquerons (ainsi que la fonction locals à l'air étrange) dans la
    prochaine section.
(3) Reconstruire les balises de fin est beaucoup plus simple, il suffit de
    prendre le nom de la balise est de l’encadrer de .
(4) Lorsque SGMLParser trouve une référence de caractère, il appelle
    handle_charref avec la référence. Si le document HTML contient la référence
     , ref vaudra 160. La reconstruction de la référence de caractère
    originelle ne demande que d’encadrer ref par &#...;.
(5) Les références d’entité sont semblables aux références de caractères, mais
    sans le signe dièse. La reconstruction de la référence d’entité originelle
    demande d’encadrer ref par &...;. (En fait, comme un lecteur savant me l’a
    fait remarquer, c’est un peu plus compliqué que ça. Seulement certaines
    entités standard du HTML finissent par un point-virgule. Heureusement pour
    nous, l’ensemble des entités standards est défini dans un dictionnaire dans
    un module Python appelé htmlentitydefs. C’est l’explication de
    l’instruction if supplémentaire.)
(6) Les blocs de texte sont simplement ajouté à self.pieces sans modification.
(7) Les commentaires HTML sont encadrés par .
(8) Les instructions de traitement sont encadrés par .

    Important: Traitement du HTML avec script intégré
    La spécification HTML exige que tous les éléments non-HTML (comme le
    JavaScript côté client) soient compris dans des commentaires HTML, mais
    toutes les pages web ne le font pas (et les navigateurs web récents ne
    l’exigent pas). BaseHTMLProcessor, lui, l’exige, si le script n’est
    correctement encadré dans un commentaire, il sera analysé comme s’il était
    du code HTML. Par exemple, si le script contient des signes inférieurs à ou
    égal, SGMLParser peut considérer à tort qu’il a trouvé des balises et des
    attributs. SGMLParser convertit toujours les noms de balises et d’attributs
    en minuscules, ce qui peut empêcher la bonne exécution du script et
    BaseHTMLProcessor entoure toujours les valeurs d’attributs entre guillemets
    (même si le document HTML n’en utilisait pas ou utilisait des guillemets
    simples), ce qui empêchera certainement l’exécution du script. Protégez
    toujours vos script côté client par des commentaires HTML.

Exemple 8.9. Sortie de BaseHTMLProcessor

    def output(self):               (1)
        """Return processed HTML as a single string"""
        return "".join(self.pieces) (2)

(1) Voici l’unique méthode de BaseHTMLProcessor qui n’est jamais appelée par
    son ancêtre, SGMLParser. Comme les méthodes de prise en charge stockent le
    HTML reconstitué dans self.pieces, cette fonction est nécessaire pour
    assembler toutes ces pièces en une chaîne unique. Comme noté précédemment,
    Python est bon pour gérer les listes et moyens pour les chaînes, nous ne
    créons donc la chaîne seulement quand un utilisateur la réclame
    explicitement.
(2) Si vous préférez, vous pouvez plutôt utiliser la méthode join du module
    string : string.join(self.pieces, "")

Pour en savoir plus

  • Le W3C (http://www.w3.org/) traite des références de caractères et
    d’entités (http://www.w3.org/TR/REC-html40/charset.html#entities).
  • La Python Library Reference (http://www.python.org/doc/current/lib/)
    confirme vos soupçons selon lesquels le module (http://www.python.org/doc/
    current/lib/module-htmlentitydefs.html) est exactement ce que son nom
    laisse deviner.

8.5. locals et globals

Laissons de coté le traitement du HTML une minute pour parler de la manière
dont Python gère les variables. Python a deux fonctions prédéfinies permettant
d’accéder aux variables locales et globales sous forme de dictionnaire : locals
et globals.

Vous vous rappelez de locals ? Vous l'avez vu pour la première fois ici :

    def unknown_starttag(self, tag, attrs):
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())

Mais vous ne pouvez rien apprendre sur locals pour le moment. Vous devez
d'abord apprendre les espaces de noms. C’est un concept un peu aride mais
important, lisez donc attentivement.

Python utilise ce que l’on appelle des espaces de noms pour suivre les
variables. Un espace de noms est semblable a un dictionnaire dans lequel les
clés sont les noms des variables et les valeurs du dictionnaire sont les
valeurs des variables. En fait, on accède à un espace de noms comme à un
dictionnaire Python, comme nous le verrons un peu plus loin.

A n’importe quel point dans un programme Python, il y a plusieurs espaces de
noms disponibles. Chaque fonction a son propre espace de noms, appelé espace de
noms local, qui suit les variables de la fonction, y compris ses arguments et
les variables définies localement. Chaque module a son propre espace de noms,
appelé l’espace de noms global, qui suit les variables du module, y compris les
fonctions, les classes, les modules importés et les variables et constantes du
module. Il y a également un espace de noms prédéfini, accessible de n’importe
quel module et qui contient les fonctions et exceptions du langage.

Lorsqu’une ligne de code demande la valeur d’une variable x, Python recherche
cette variable dans tous les espaces de noms disponibles dans l’ordre suivant :

 1. Espace de noms local - spécifique à la fonction ou méthode de classe en
    cours. Si la fonction a défini une variable locale x, ou si elle a un
    argument x, Python l’utilise et arrête sa recherche.
 2. Espace de noms global - spécifique au module en cours. Si le module a
    défini une variable, une fonction ou une classe nommée x, Python l’utilise
    et arrête sa recherche.
 3. Espace de noms prédéfini - global à tous les modules. En dernière instance,
    Python considère que x est le nom d’une fonction ou variable du langage.

Si Python ne trouve x dans aucun de ces espaces de noms, il abandonne et
déclenche une exception NameError avec le message There is no variable named
'x', que vous avez vu tout au début au chapitre 1, mais à ce moment là vous ne
pouviez pas savoir tout le travail que Python fait avant de vous renvoyer cette
erreur.

    Important: Evolution du langage : portées imbriquées
    Python 2.2 a introduit une modification légère mais importante qui affecte
    l’ordre de recherche dans les espaces de noms : les portées imbriquées.
    Dans les versions précédentes de Python, lorsque vous référenciez une
    variable dans une fonction imbriquée ou une fonction lambda, Python
    recherchait la variable dans l’espace de noms de la fonction (imbriquée ou
    lambda) en cours, puis dans l’espace de noms du module. Python 2.2
    recherche la variable dans l’espace de noms de la fonction (imbriquée ou
    lambda) en cours, puis dans l’espace de noms de la fonction parente, puis
    dans l’espace de noms du module. Python 2.1 peut adopter l'un ou l'autre de
    ces comportements, par défaut il fonctionne comme Python 2.0, mais vous
    pouvez ajouter la ligne de code suivante au début de vos modules pour les
    faire fonctionner comme avec Python 2.2 :

    from __future__ import nested_scopes


Vous êtes perdu ? Ne vous inquiétez pas, je vous promet que c'est très utile.
Comme beaucoup de chose en Python, les espaces de noms sont directement
accessibles durant l’exécution. L’espace de noms local est accessible par la
fonction prédéfinie locals et l’espace de noms global (du module) est
accessible par la fonction prédéfinie globals.

Exemple 8.10. Présentation de locals

>>> def foo(arg): (1)
...     x = 1
...     print locals()
...
>>> foo(7)        (2)
{'arg': 7, 'x': 1}
>>> foo('bar')    (3)
{'arg': 'bar', 'x': 1}

(1) La fonction foo a deux variables dans son espace de noms local : arg, dont
    la valeur est passée à la fonction et x, qui est définie dans la fonction.
(2) locals retourne un dictionnaire de paires nom/valeur. Les clés du
    dictionnaire sont les noms des variables sous forme de chaînes, les valeurs
    du dictionnaire sont les valeurs des variables. Donc, appeler foo avec 7
    affiche un dictionnaire contenant les deux variables locales de la fonction
    : arg (7) et x (1).
(3) Rappelez-vous que Python est à typage dynamique, vous pouvez donc passer
    une chaîne pour arg, la fonction (et l’appel à locals) fonctionne tout
    aussi bien. locals fonctionne avec toutes les variables de tous les types.

Ce que fait locals pour l’espace de noms local (de la fonction), globals le
fait pour l’espace de noms global (du module). globals est cependant plus
intéressant, parce que l’espace de noms d’un module est plus intéressant.
L’espace de noms d’un module ne comprend pas seulement les variables et
constantes du module mais aussi l’ensemble des fonctions et classes définies
dans le module. De plus, il contient tout ce qui a été importé dans le module.

Vous rappelez-vous de la différence entre from module import et import module ?
Avec import module, le module lui-meme est importé mais il garde son propre
espace de noms, c’est pourquoi vous devez utiliser le nom du module pour
accéder à ses fonctions ou attributs : module.fonction. Mais avec from module
import, vous importez des fonctions et des attributs spécifiques d’un autre
module dans votre propre espace de noms, c’est pourquoi vous y accédez
directement sans référence au module dont ils viennent. Avec la fonction
globals, vous pouvez voir ce qui se passe.

Exemple 8.11. Présentation de globals

Regardez me bloc de code suivant, à la fin de BaseHTMLProcessor.py:

if __name__ == "__main__":
    for k, v in globals().items():             (1)
        print k, "=", v

(1) Ne soyez pas intimidé, vous avez déjà vu tout cela. La fonction globals
    retourne un dictionnaire et nous parcourons le dictionnaire à l’aide de la
    méthode items et de l’assignement multiple. Le seul élément nouveau ici est
    la fonction globals.

Maintenant, exécuter le programme de la ligne de commande nous donne la sortie
suivante (notez qu'elle peut être légèrement différente en fonction de votre
plate-forme et de l'endroit où vous avez installé Python) :

c:\docbook\dip\py> python BaseHTMLProcessor.py

SGMLParser = sgmllib.SGMLParser                (1)
htmlentitydefs =  (2)
BaseHTMLProcessor = __main__.BaseHTMLProcessor (3)
__name__ = __main__                            (4)
... rest of output omitted for brevity...

(1) SGMLParser a été importé de sgmllib, en utilisant from module import. Cela
    veut dire qu’il a été importé directement dans l’espace de noms du module,
    et nous le voyons donc s’afficher.
(2) Comparez avec htmlentitydefs, qui a été importé avec import. Le module
    htmlentitydefs lui-même est dans notre espace de noms, mais la variable
    entitydefs définie dans htmlentitydefs ne l’est pas.
(3) Ce module ne définit qu’une classe, BaseHTMLProcessor et la voici. Notez
    que la valeur est ici la classe elle-même et non une instance quelconque de
    cette classe.
(4) Vous rappelez-vous de l’astuce if __name__ ? Lorsque vous exécutez un
    module (plutôt que de l’importer d’un autre module), l’attribut prédéfini
    __name__ a une valeur spéciale, __main__. Comme nous avons exécuté ce
    module comme un programme de la ligne de commande, __name__ vaut __main__,
    c’est pourquoi notre petit code de test qui affiche globals est exécuté.

    NOTE: Accéder aux variables dynamiquement
    A l’aide des fonctions locals et globals, vous pouvez obtenir la valeur
    d’une variable quelconque dynamiquement, en fournissant le nom de la
    variable sous forme de chaîne. C’est une fonctionnalité semblable à celle
    de la fonction getattr, qui vous permet d’accéder à une fonction quelconque
    dynamiquement en fournissant le nom de la fonction sous la forme d’une
    chaîne.

Il y a une différence importante entre locals et globals que vous devez
apprendre maintenant pour ne pas qu’elle vous joue des tours plus tard. Elle
vous jouera des tours de toute manière mais au moins vous vous souviendrez que
vous l’avez appris.

Exemple 8.12. locals est en lecture seule, globals ne l'est pas

def foo(arg):
    x = 1
    print locals()    (1)
    locals()["x"] = 2 (2)
    print "x=",x      (3)

z = 7
print "z=",z
foo(3)
globals()["z"] = 8    (4)
print "z=",z          (5)

(1) Puisque foo est appelé avec 3 en paramètre, cela affichera {'arg': 3, 'x':
    1}. Cela ne devrait pas surprendre.
(2) locals est une fonction qui retourne un dictionnaire et ici vous changez
    une valeur dans ce dictionnaire. Vous pourriez penser que cela change la
    valeur de la variable locale x à 2, mais ce n’est pas le cas. locals ne
    retourne pas vraiment l’espace de noms local mais une copie, modifier le
    dictionnaire retourné ne change pas les variables de l’espace de noms
    local.
(3) Ceci affiche x= 1, pas x= 2.
(4) Après avoir été déçu par locals, vous pourriez penser que cela ne va pas
    changer la valeur de z, mais ce serait une erreur. Pour des raisons ayant
    trait à l’implémentation de Python (dans le détail desquelles je ne
    rentrerais pas, ne les comprenant pas totalement), globals retourne
    l’espace de noms global lui-même et non une copie, le comportement inverse
    de locals. Donc, toute modification du dictionnaire retourné par globals
    affecte les variables globales.
(5) Ceci affiche z= 8, pas z= 7.

8.6. Formatage de chaînes à l’aide d’un dictionnaire

Pourquoi avoir appris locals et globals ? Pour apprendre le formatage de
chaînes à l'aide d'un dictionnaire. Comme vous vous le rappelez, le formatage
de chaînes permet d’insérer facilement des valeurs dans des chaînes. Les
valeurs sont énumérées dans un tuple et insérées dans l’ordre dans la chaîne à
la place de chaque marqueur de formatage. Bien que ce soit efficace, cela ne
donne pas le code le plus simple à lire, surtout quand de multiples valeurs
sont insérées. Vous ne pouvez pas simplement lire la chaîne en une fois pour
comprendre ce que le résultat va être, vous devez constamment passer de la
chaîne au tuple.

Il existe une technique de formatage de chaînes alternative utilisant un
dictionnaire au lieu de valeurs stockées dans un tuple.

Exemple 8.13. Présentation du formatage de chaînes à l’aide d’un dictionnaire

>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> "%(pwd)s" % params                                    (1)
'secret'
>>> "%(pwd)s is not a good password for %(uid)s" % params (2)
'secret is not a good password for sa'
>>> "%(database)s of mind, %(database)s of body" % params (3)
'master of mind, master of body'

(1) Au lieu d’un tuple de valeurs explicites, ce type de formatage de chaînes
    utilise un dictionnaire, params. Et au lieu d’un simple marqueur %s dans la
    chaîne, le marqueur contient un nom entre parenthèses. Ce nom est utilisé
    comme clé dans le dictionnaire params et la valeur correspondante, secret,
    est substituée au marqueur %(pwd)s.
(2) Le formatage à l’aide d’un dictionnaire fonctionne avec n’importe quel
    nombre de clés nommées. Chaque clé doit exister dans le dictionnaire, sinon
    le formatage échouera avec une erreur KeyError.
(3) Vous pouvez même insérer la même clé plusieurs fois, chaque occurrence sera
    remplacée avec la même valeur.

Quand utiliser le formatage à l’aide d’un dictionnaire ? Il est un peu exagéré
de mettre en place un dictionnaire de clés et de valeurs simplement pour
formater une ligne, c’est en fait plus approprié lorsque vous avez déjà un
dictionnaire. Comme par exemple locals.

Exemple 8.14. Formatage à l’aide d’un dictionnaire dans BaseHTMLProcessor.py

    def handle_comment(self, text):
        self.pieces.append("" % locals()) (1)

(1) L’utilisation de la fonction prédéfinie locals est le cas le plus commun
    d’emploi du formatage à l’aide d’un dictionnaire. Cela vous permet
    d’utiliser les noms des variables locales dans votre chaîne (dans ce cas,
    text, qui a été passé en argument à la méthode de classe) et chaque
    variable sera remplacée par sa valeur. Si text est 'Begin page footer', le
    formatage de chaîne "" % locals() se traduira par la chaîne
    ''.

Exemple 8.15. Autres exemples de formatage à l’aide d’un dictionnaire

    def unknown_starttag(self, tag, attrs):
        strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs]) (1)
        self.pieces.append("<%(tag)s%(strattrs)s>" % locals())                      (2)

(1) Lorsque cette méthode est appelée, attrs est une liste de tuples clé/
    valeur, comme les items (éléments) d’un dictionnaire, ce qui signifie que
    nous pouvons utiliser l’assignement multiple pour la parcourir. Cela
    devrait être un motif familier maintenant, mais il se passe beaucoup de
    choses ici, détaillons-les :
     a. Supposez que attrs vaut [('href', 'index.html'), ('title', 'Go to home
        page')].
     b. Durant la première étape de la list comprehension, la clé (key) sera
        'href' et la valeur (value) 'index.html'.
     c. Le formatage de chaîne ' %s="%s"' % (key, value) donnera ' href=
        "index.html"'. Cette chaîne devient le premier élément de la valeur de
        retour de la list comprehension.
     d. Durant la seconde étape, key sera 'title' et value 'Go to home page'.
     e. Le formatage de chaîne donnera ' title="Go to home page"'.
     f. La list comprehension retourne une liste de ces deux chaînes et
        strattrs joindra les deux éléments de cette liste pour former ' href=
        "index.html" title="Go to home page"'.
(2) Maintenant, en utilisant le formatage à l’aide d’un dictionnaire, nous
    insérons la valeur de tag et de strattrs dans une chaîne. Donc si tag vaut
    'a', le résultat final sera ''
    et c’est ce qui sera ajouté à self.pieces.

    Important: Problèmes de performances avec locals
    L’utilisation du formatage de chaîne à l’aide d’un dictionnaire avec locals
    est une manière pratique de rendre des expressions de formatage complexes
    plus lisibles, mais elle a un prix. Il y a une petite baisse de performance
    due à l’appel de locals, puisque locals effectue une copie de l’espace de
    noms local.

8.7. Mettre les valeurs d’attributs entre guillemets

Une question courante sur comp.lang.python (http://groups.google.com/groups?
group=comp.lang.python) est la suivante : <>^[5] (C’est en général du à un chef de projet qui pratique la
religion du HTML-est-un-standard et proclame que toutes les pages doivent
passer les tests d’un validateur HTML. Les valeurs d’attributs sans guillemets
sont une violation courante du standard HTML). Quelle que soit la raison, les
valeurs d’attributs peuvent se voir dotées de guillemets en soumettant le HTML
à BaseHTMLProcessor.

BaseHTMLProcessor prend du HTML en entrée (puisqu’il est dérivé de SGMLParser)
et produit du HTML, mais le HTML en sortie n’est pas identique à l’entrée. Les
balises et les noms d’attributs sont mis en minuscules et les valeurs
d’attributs sont mises entre guillemets, quel qu’ait été les format en entrée.
C’est de cet effet de bord que nous pouvons profiter.

Exemple 8.16. Mettre les valeurs d’attributs entre guillemets

>>> htmlSource = """        (1)
...     
...     
...     Test page
...     
...     
...     
diveintopython.org
Python for experienced programmers