Plongée dans Python

Mark Pilgrim

Traducteurs

| TJ1985 | Guillaume LEFEBVRE | erca57 | Julien N | o_Nejma |

Python est un langage de programmation objet, multiparadigme et multiplateforme. Ce cours est un excellent tutoriel pour débuter avec Python 3 et la programmation. C'est un cours qui se propose de vous apprendre les bases de la programmation Python ainsi que les notions avancées.

Commentez

- Titre : Plongée dans Python

- Auteur : Mark Pilgrim

- Traducteurs : TJ1985, Guillaume LEFEBVRE, erca57, Julien N, o_Nejma

- Parution : 21 janvier 2019

- Mise à jour : 6 septembre 2021

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Mark Pilgrim. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Chapitre 0. Installer Python

- I-A. Quel est le Python qui vous convient ?

- I-B. Installation sous Microsoft Windows

- I-C. Installation sur macOS

- I-D. Installer sur Ubuntu Linux

- I-E. Installer sur d’autres plateformes

- I-F. Utiliser le shell Python

- I-G. Les éditeurs Python et les IDE

- II. Chapitre 1 : Votre premier programme Python

- II-A. Allons y !

- II-B. Déclarer une fonction

- II-B-1. Arguments nommés et optionnels

- II-C. Écrire du code lisible : la  docstring

- II-D. La recherche du chemin d’import

- II-E. Tout est objet

- II-F. Indenter son code

- II-G. Les exceptions

- II-G-1. Intercepter les erreurs d'importation de module

- II-H. Variables non initialisées

- II-I. Tout est sensible à la casse

- II-J. Lancer un script

- II-K. Pour aller plus loin (en anglais)

- III. Chapitre 2 – Les types de données natifs

- III-A. Plongeons

- III-B. Les booléens

- III-C. Les nombres

- III-C-1. Convertir des entiers en flottants et vice-versa

- III-C-2. Opérations numériques courantes

- III-C-3. Fractions

- III-C-4. Trigonométrie

- III-C-5. Les nombres dans un contexte booléen.

- III-D. Les Listes

- III-D-1. Créer une liste

- III-D-2. Découper une liste

- III-D-3. Ajouter des éléments à une liste

- III-D-4. Rechercher des valeurs dans une liste

- III-D-5. Retirer des éléments d’une liste

- III-D-6. Retirer des éléments d’une liste : un tour en prime

- III-D-7. Les listes dans un contexte booléen

- III-E. Les Tuples

- III-E-1. Les tuples dans un contexte booléen

- III-E-2. Affecter de multiples valeurs à la fois

- III-F. Les ensembles (ou Sets)

- III-F-1. Créer un ensemble

- III-F-2. Modifier un ensemble

- III-F-3. Retirer des éléments d’un ensemble

- III-F-4. Opérations courantes sur les ensembles

- III-F-5. Les ensembles dans un contexte booléen

- III-G. Les dictionnaires (dictionaries)

- III-G-1. Créer un dictionnaire

- III-G-2. Modifier un dictionnaire

- III-G-3. Dictionnaires à valeurs mélangées

- III-G-4. Dictionnaires dans un contexte booléen

- III-H. None

- III-H-1. None dans un contexte booléen

- III-I. Pour aller plus loin

- IV. Chapitre 3 : Compréhensions de listes, de dictionnaires et de sets

- IV-A. Plongée dans Python

- IV-B. Travailler avec des fichiers et répertoires

- IV-B-1. Le répertoire courant

- IV-B-2. Travailler avec des noms de fichiers et de dossiers

- IV-B-3. Lister des répertoires

- IV-B-4. Récupérer les métadonnées d'un fichier

- IV-B-5. Construire un chemin absolu

- IV-C. Compréhensions de listes (liste_intention)

- IV-D. Compréhensions de dictionnaires

- IV-D-1. D'autres choses sympathiques à faire avec les compréhensions de dictionnaires

- IV-E. Compréhensions de sets

- IV-F. Lectures pour aller plus loin

- V. Chapitre 4 - Strings

- V-A. Quelques petites choses ennuyeuses que vous devez comprendre avant de plonger

- V-B. Unicode

- V-C. Plongeons

- V-D. Formater les chaînes de caractères

- V-D-1. Les champs de noms composés

- V-D-2. Les spécificateurs de format

- V-E. D'autres méthodes communes des chaînes de caractères

- V-E-1. Découper des chaînes de caractères

- V-F. Chaînes de caractères vs. octets

- V-G. Postscriptum : encodage de caractères du code source de Python

- V-H. Lectures complémentaires

- VI. Chapitre 5 – Expressions Régulières

- VI-A. PLONGER DANS

- VI-A-1. Étude de cas : les adresses de rues

- VI-B. Case Study: Roman Numerals

- VI-B-1. Recherche de milliers

- VI-B-2. Vérification des centaines

- VI-C. Utilisation de la syntaxe {n, m}

- VI-C-1. Vérification des dizaines et des unités

- VI-C-2. Expressions régulières verbeuses

- VI-C-3. Étude de cas : analyse des numéros de téléphone

- VI-C-4. Résumé

- VII. Chapitre 6 : closures et générateurs

- VII-A. En plongée

- VII-B. Je sais, utilisons les expressions régulières !

- VII-C. Une liste de fonctions

- VII-D. Une liste de motifs

- VII-E. Un fichier de motifs

- VII-F. Générateurs

- VII-F-1. Un générateur Fibonacci

- VII-F-2. Un générateur de règles du pluriel

- VII-G. Pour aller plus loin (en anglais)

I - Chapitre 0. Installer Python

Tempora mutantur nos et mutantur in illis. (Les Temps changent et nous changeons avec eux.)

— ancien proverbe Romain.

Avant de pouvoir utiliser Python, nous devons l’installer. D’accord ?

I-A - Quel est le Python qui vous convient ?

Si vous utilisez un compte hébergé sur un serveur, votre F.A.I. vous aura certainement installé Python 3. Si vous fonctionnez sous Linux, vous aurez sans doute aussi Python 3. La plupart des distributions GNU/Linux viennent avec Python 2 comme installation par défaut. Un petit nombre, mais croissant, de distributions incluent aussi Python 3. MacOS inclut une version ligne de commande de Python 2, mais au moment de la rédaction ne propose pas Python 3. Microsoft Windows est fourni sans aucune version de Python. Mais ne désespérez pas ! Vous pourrez toujours trouver un moyen simple d’installer Python, quel que soit votre système d’exploitation.

La manière la plus simple de vérifier la version de Python installée sur votre machine Linux ou macOS est de passer par la ligne de commande. Une fois que vous êtes au prompt, saisissez juste python3 (tout en minuscules, sans espace), puis ENTER et observez ce qui se passe. Sur mon système Linux domestique Python 3.1 est installé, et cette commande me donne accès au shell python interactif.

mark@atlantis:~$ python3
Python 3.1 (r31:73572, Jul 28 2009, 06:52:23)
[GCC 4.2.4 (Ubuntu 4.2.4-1ubuntu4)] on linux2

Saisissez exit() et pressez ENTER pour sortir du shell interactif Python.

Bien, revenons à la question du début de cette section, « Quelle version de Python est la bonne pour vous ? » Celle qui fonctionne sur la machine que vous avez déjà.

[Continuez à lire pour l’installation sous Windows, ou passez directement à l’installation sous macOS, sous Ubuntu ou d’autres plateformes]

I-B - Installation sous Microsoft Windows

Windows est fourni en deux versions : 32 bits et 64 bits. Bien sûr il existe beaucoup de versions différentes de Windows – XP, Vista, Windows 7, Windows 8, Windows 10 – mais Python tourne sur chacune d’elles. La distinction la plus importante est entre 32 et 64 bits. Si vous n’avez aucune idée de l’architecture sur laquelle vous tournez, c’est probablement du 32 bits, avant Windows 10.

Note de la rédaction : une version Python 32 bits fonctionnera sur un windows 64 bits.

Connectez-vous à python.org/download/ et téléchargez la version appropriée de l’installeur Python 3 pour Windows et pour votre architecture. Vous aurez le choix entre quelque chose comme ceci :

- Python 3.1 Windows installer (Windows binary — does not include source) ;

- Python 3.1 Windows AMD64 installer (Windows AMD64 binary — does not include source).

Je ne veux pas inclure de liens de téléchargement direct ici, car des changements apparaissent souvent pour Python et je ne voudrais pas vous faire manquer une mise à jour importante. Vous devriez toujours installer la version la plus récente de Python 3.x, sauf si vous avez une raison ésotérique de ne pas le faire.

Une fois le téléchargement terminé, double-cliquez sur le fichier .msi. Windows vous présentera un message d’alerte de sécurité, puisque vous vous apprêtez à exécuter du code. L’installateur Python officiel est signé numériquement par la Python Software Foundation, la société sans but lucratif qui supervise le développement de Python. N’acceptez pas d’imitation ! Cliquez le bouton Run pour démarrer l’installation de Python 3.

L’installeur vous demandera avant tout si vous voulez installer Python 3 pour tous les utilisateurs ou seulement pour vous. Le choix par défaut est l’installation pour tous les utilisateurs, qui est le meilleur à moins que vous n’ayez de solides raisons de ne pas le faire (une des bonnes raisons de n’installer que pour vous est si vous installez Python sur l’ordinateur de votre entreprise dont vous n’êtes pas administrateur. Mais si vous faites cela, pourquoi le faites-vous sans l’autorisation de votre administrateur Windows ? Ne me causez pas d’ennuis ! Cliquez sur le bouton Next pour accepter le choix du type d’installation.

Ensuite l’installeur vous demandera de choisir un répertoire de destination. Par défaut toutes les versions de Python 3.1.x s’installent dans C:\Python31\, ce qui devrait convenir à la plupart des utilisateurs, sauf si vous avez une raison spécifique d’en changer. Si vous réservez un disque séparé pour les installations, vous pouvez parcourir l’arborescence de vos disques en utilisant les contrôles fournis, ou simplement saisir le chemin désiré dans la boîte du dessous. Vous n’êtes pas tenus d’installer Python sur le disque C :, vous pouvez l’installer sur n’importe quel disque, dans n’importe quel dossier. Cliquez sur le bouton Next pour valider le choix du répertoire de destination.

La page suivante semble compliquée, mais elle ne l’est pas réellement. Comme pour beaucoup d’installeurs, vous avez la possibilité de ne pas installer chaque composant de Python 3. Si vous êtes très contraint en espace disque, vous pouvez exclure certains composants de l’installation.

- Register Extensions vous permet d’exécuter un script Python, d’extension .py en double-cliquant sur son fichier. Recommandé mais non requis. Cette extension ne coûte pas d’espace disque, donc l’exclure n’apporte rien à ce niveau.

- Tcl/Tk est la bibliothèque graphique utilisée par le shell Python, que vous utiliserez tout au long de ce livre. Je recommande fortement de conserver cette option.

- Documentation installe un fichier d’aide qui contient une grande part des informations de docs.python.org. Recommandé si vous avez un accès limité à l’Internet.

- Utility scripts inclut le script 2to3.py que nous verrons plus loin dans ce livre. Requis si vous voulez voir comment migrer du code existant Python2 vers Python3. Si vous n’avez pas de code Python2 vous pouvez sauter cette option.

- Test Suite est une collection de scripts utilisés pour tester l’interpréteur Python lui-même. Nous ne l’utiliserons pas dans ce livre, et je ne l’ai jamais utilisée non plus dans le cours « Programming in Python ». Complètement optionnel.

Si vous ne savez pas exactement de combien d’espace disque vous disposez, cliquez sur le bouton Disk Usage. L’installeur affichera vos disques, calculera combien d’espace est disponible sur chacun d'eux et combien d’espace restera après l’installation. Cliquez sur le bouton OK pour revenir à la page « Personnalisation de Python ».

Si vous décidez d’exclure une option, choisissez le bouton déroulant devant l’option et choisissez « La fonction entière sera indisponible ». Par exemple, exclure Test Suite vous sauvera un merveilleux 7908 Ko d’espace disque. Cliquez sur le bouton Next pour accepter votre choix d’options.

L’installeur copiera les fichiers nécessaires vers le répertoire de destination (ça va si vite que j’ai dû m’y reprendre à trois fois pour obtenir une copie d’écran de cette installation!).

Cliquez sur le bouton Finish pour sortir de l’installeur.

Dans votre menu Démarrer vous devez trouver un nouvel élément nommé Python 3.1. À l’intérieur vous devez avoir un programme nommé IDLE. Choisissez-le pour démarrer le shell Python interactif.

[Passer à Utiliser le shell Python]

I-C - Installation sur macOS

Tous les MacIntosh modernes utilisent des processeurs Intel (comme la plupart des PC Windows). Les Macs plus anciens utilisaient des processeurs PowerPC. Vous n’avez pas besoin de comprendre la différence, car il n’y a qu’un installeur Python pour tous les Macs.

Allez sur python.org/download/ et téléchargez l’installeur Mac. Il sera nommé quelque chose comme Python 3.1 Mac Installer Disk Image, même si la version peut varier. Mais assurez-vous de bien télécharger la version 3.x, et non 2.x.Votre explorateur Internet doit automatiquement monter une image disque et ouvrir une fenêtre du Finder pour vous en montrer le contenu. Si ça ne marche pas ainsi, vous devez rechercher le fichier téléchargé dans votre dossier de téléchargement et double-cliquer pour le monter. Il sera nommé quelque chose comme python-3.1.dmg. L’image disque contient un certain nombre de fichiers texte (Build.txt, License.txt, ReadMe.txt) et l’installeur Python lui-même, Python.mpkg. Double-cliquez sur ce dernier pour démarrer l’installeur Python pour Mac.

La première page de l’installeur vous donne une brève description de Python lui-même, et vous renvoie au fichier ReadMe.txt (que vous n’avez pas lu, n’est-ce pas?) pour plus de détails. Cliquez sur le bouton Continue pour poursuivre.

La page suivante contient des informations vraiment importantes : Python exige macOS 10.3 ou plus récent. Si vous tournez encore sous macOS 10.2 ou plus ancien vous devez vraiment faire la mise à jour. Apple ne fournit plus de mises à jour de sécurité pour votre système et votre machine est sans doute exposée au risque si vous êtes connecté à l’Internet. Vous ne pouvez pas non plus exécuter Python 3. Cliquez sur le bouton Continue pour poursuivre.

Comme tout bon installeur, l’installeur Python affiche l’accord de licence logicielle. Python est Open Source, et sa licence est approuvée par l’initiative Open Source. Python a eu plusieurs propriétaires et sponsors au long de son histoire, chacun d’eux ayant laissé sa trace sur la licence logicielle. Mais à la fin le résultat est celui-ci : Python est Open Source et vous pouvez l’utiliser sur toute plateforme, dans n’importe quel but, sans frais ni obligation de réciprocité. Cliquez encore une fois sur le bouton Continue.

En raison de bizarreries dans le framework standard d’installation d’Apple vous devez « accepter » la licence logicielle pour procéder à l’installation. Puisque Python est OpenSource, vous acceptez en fait que la licence vous donne des droits additionnels plutôt qu’elle ne vous enlève. Cliquez sur le bouton  Agree  pour continuer.

L’écran suivant vous permet de modifier la destination de l’installation. Vous Devez installer Python sur votre disque de démarrage, mais en raison de limitations de l’installeur, il ne vous y contraint pas. En vérité, je n’ai jamais eu à modifier la destination de l’installation. À partir de cet écran vous pouvez aussi personnaliser l’installation pour exclure certaines fonctions. Si vous le souhaitez, cliquez sur le bouton Customize, sinon cliquez sur Install.

Si vous choisissez une installation personnalisée, l’installeur vous proposera la liste d’options suivante :

- Python Framework : c’est le corps de Python, et il est sélectionné et non modifiable, car il doit être installé.

- GUI Applications : inclut Idle, le shell graphique Python que vous utiliserez tout au long de ce livre. Je recommande fortement de garder cette option.

- UNIX command-line tools : inclut l’application en ligne de commande python3. Je recommande fortement de conserver cette option, également.

- Python Documentation : contient la plupart des informations de docs.python.org. Recommandé si votre liaison Internet est à faible débit.

- Shell profile updater : contrôle si vous devez mettre à jour votre profil shell (utilisé par Terminal.app) pour garantir que cette version de Python est dans le chemin de recherche de votre shell. Vous n’avez probablement pas besoin de modifier cette option.

- Fix system Python : ne devrait pas être modifié. Il indique au Mac d’utiliser Python 3 par défaut pour les scripts Python, y compris ceux inclus dans le système fournis par Apple. Les conséquences peuvent être néfastes, puisque la plupart de ces scripts sont écrits en Python2 et ne s’exécuteront pas correctement en Python 3.

Cliquez sur le bouton Install pour continuer.

Comme l’installeur place les frameworks globaux et les binaires dans /usr/local/bin/, il vous demandera un mot de passe administrateur. Il n’est pas possible d’installer Python sans les privilèges administrateur. Cliquez sur le bouton OK pour commencer l’installation.

L’installeur affichera une barre de progression pendant qu’il installe les fonctionnalités que vous avez choisies.

Si tout se passe bien, l’installeur affichera une grande coche verte pour vous prévenir que l’installation s’est achevée correctement. Cliquez sur le bouton Close.

Si vous avez installé le logiciel dans son répertoire par défaut, vous pouvez trouver les fichiers fraîchement installés dans le répertoire Python 3.1, dans votre dossier /Applications. Le plus important est idle, le shell graphique Python. Double-cliquez sur idle pour démarrer le shell Python.

Le shell Python est l’endroit où vous passerez le plus de temps en explorant Python. Les exemples de ce livre supposent que vous êtes à l’aise dans celui-ci.

[Passer à Utiliser le shell Python]

I-D - Installer sur Ubuntu Linux

Les distributions Linux modernes s’appuient sur de vastes répertoires d’applications précompilées prêtes à être installées. Les détails exacts varient en fonction de la distribution. Pour Ubuntu Linux, la manière la plus simple d’installer Python 3 est de passer par Ajout/Suppression d’application dans votre menu Applications.

Lorsque vous démarrez pour la première fois Ajout/Suppression d’application, il vous montrera une liste d’applications présélectionnées de différentes catégories. Certaines sont déjà installées, d’autres non. Comme le répertoire contient plus de 10 000 applications, il est possible d’appliquer des filtres pour sélectionner des tranches raisonnables du répertoire. Le filtre par défaut est « Applications maintenues par Canonical » qui définit un sous-ensemble restreint d’applications, maintenu officiellement par Canonical, la compagnie qui a créé et maintient Ubuntu Linux.

Python 3 n’est pas maintenu officiellement par Canonical, donc la première chose à faire est de choisir le filtre « Toutes les Applications OpenSource ».

Une fois le filtre élargi à l’ensemble des applications OpenSource, utilisez la boîte de recherche placée immédiatement sous le filtre pour rechercher Python 3.

Maintenant, la liste des applications se restreint à celles dont le nom correspond à Python 3. Vous devez choisir deux paquets. Le premier est Python (v3.0). C’est l’interpréteur Python proprement dit.

Le second paquet que vous voulez est immédiatement en-dessous : IDLE (pour Python-3.0). C’est un shell graphique Python que vous utiliserez tout au long de ce livre. Une fois que vous avez sélectionné ces deux paquets, cliquez sur le bouton Apply Changes pour continuer.

Le gestionnaire de paquets vous demandera de confirmer que vous voulez bien ajouter IDLE (using Python-3.0) et Python (v3.0). Cliquez sur le bouton Apply pour continuer.

Le gestionnaire de paquets vous montrera une barre de progression affichant le téléchargement des paquets nécessaires depuis le répertoire Internet de Canonical.

Lorsque les paquets auront été téléchargés, le gestionnaire de paquets commencera automatiquement à les installer.

Si tout se passe bien, le gestionnaire de paquets confirmera que les deux paquets ont été installés avec succès. À partir de là, vous pouvez cliquer sur idle pour démarrer le shell Python, ou cliquez sur le bouton Close pour sortir du gestionnaire de paquets. Vous pouvez toujours redémarrer le shell Python en allant dans le menu « Applications », sous-menu « Programmation », et en choisissant idle.

Le shell Python est l’endroit où vous passerez le plus de temps à explorer Python. Les exemples de ce livre partent de l’idée que vous êtes à l’aise dans le shell Python.

[ Passer à Utiliser le shell Python]

I-E - Installer sur d’autres plateformes

Python 3 est disponible pour nombres d’autres plateformes. En particulier il est disponible pour virtuellement toute distribution basée sur Linux, BSD ou Solaris. Par exemple, le Linux de RedHat utilise le gestionnaire de paquets yum. FreeBSD a sa collection de portages et de paquets, Suse utilise zypper et Solaris pkgadd. Une rapide recherche sur le WEB pour Python 3 + <le nom de votre système d’exploitation> devrait vous dire si un paquet Python est disponible et si oui, comment l’installer.

I-F - Utiliser le shell Python

Le shell Python est l’endroit d’où vous pouvez explorer la syntaxe Python, obtenir de l’aide interactive sur les commandes et déboguer de petits programmes. Le shell graphique Python (nommé IDLE) propose aussi un éditeur de textes décent qui supporte la coloration syntaxique et s’intègre au shell Python proprement dit. Si vous n’avez pas d’éditeur de textes favori, vous devriez essayer IDLE.

Commençons par le début. Le shell Python lui-même est un emplacement de jeu interactif amusant. Tout au long de ce livre vous verrez des exemples, tels que celui-ci :

>>> 1 + 1
2

Les trois chevrons, >>>, signalent le prompt du shell Python. Ne saisissez pas cette partie. Ils sont là juste pour vous faire savoir que cet exemple doit être fait dans le shell Python.

1 + 1 est la partie que vous saisissez. Vous pouvez entrer n’importe quelle expression ou commande Python valable dans le shell Python. N’ayez pas peur : ça ne fait pas mal ! Le pire qui puisse arriver est que vous receviez un message d’erreur. Les commandes sont exécutées immédiatement (dès que vous pressez ENTER) ; les expressions sont évaluées immédiatement et le shell Python vous affiche le résultat.

2 est le résultat de l’évaluation de cette expression. Il apparaît que 1 + 1 est une expression Python valable. Le résultat est 2, bien sûr.

Essayons-en une autre :

>>> print('Hello world!')
Hello world!

Plutôt simple, non ? Mais vous pouvez faire beaucoup plus dans le shell Python. Si vous vous retrouvez coincé, vous ne vous rappelez plus d’une commande, ou vous ne vous rappelez plus des arguments à passer à une fonction, vous pouvez obtenir une aide interactive dans le shell Python. Entrez juste help et validez.

>>> help
Type help() for interactive help, or help(object) for help about object.

L’aide fonctionne selon deux modes. Vous pouvez obtenir de l’aide pour un objet simple, qui affichera la documentation et reviendra au prompt du shell Python. Vous pouvez aussi entrer dans le help mode, où à la place d’évaluer des expressions Python, vous saisissez des mots-clés ou des noms de commandes et il vous affichera tout ce qu’il connaît à leur propos.

Pour activer le mode d’aide interactif, saisissez help() et validez.

1. >>> help()

2. Welcome to Python 3.0!  This is the online help utility.

4. If this is your first time using Python, you should definitely check out

5. the tutorial on the Internet at http://docs.python.org/tutorial/.

7. Enter the name of any module, keyword, or topic to get help on writing

8. Python programs and using Python modules.  To quit this help utility and

9. return to the interpreter, just type "quit".

11. To get a list of available modules, keywords, or topics, type "modules",

12. "keywords", or "topics".  Each module also comes with a one-line summary

13. of what it does; to list the modules whose summaries contain a given word

14. such as "spam", type "modules spam".

16. help>

Notez que le prompt a changé de >>> à help>. Cela vous rappelle que vous êtes dans le mode d’aide interactif. Maintenant vous pouvez saisir n’importe quel mot-clé, commande, nom de module, nom de fonction : à peu près tout ce que Python comprend et lire la documentation à son propos.

Sauter cette portion de code.

1. help> print                                                                 (1)

2. Help on built-in function print in module builtins:

4. print(...)

5. print(value, ..., sep=' ', end='\n', file=sys.stdout)

7. Prints the values to a stream, or to sys.stdout by default.

8. Optional keyword arguments:

9. file: a file-like object (stream); defaults to the current sys.stdout.

10. sep:  string inserted between values, default a space.

11. end:  string appended after the last value, default a newline.

13. help> PapayaWhip                                                            (2)

14. no Python documentation found for 'PapayaWhip'

16. help> quit                                                                  (3)

18. You are now leaving help and returning to the Python interpreter.

19. If you want to ask for help on a particular object directly from the

20. interpreter, you can type "help(object)".  Executing "help('string')"

21. has the same effect as typing a particular string at the help> prompt.

22. >>>                                                                         (4)

(1) Pour obtenir de la documentation sur la fonction print(), saisir simplement print() et appuyez sur la touche ENTRER. Le mode interactif d'aide affichera des informations semblables à une page de manuel : le nom de la fonction, un bref synopsis, les arguments de la fonction et leurs valeurs par défaut, etc. Si la documentation vous semble peu claire, ne paniquez pas. Vous en apprendrez davantage sur tous ces concepts dans les prochains chapitres.

(2) Bien évidemment, le mode d'aide interactif ne sait pas tout. Si vous saisissez quelque chose qui n'est pas une commande Python, un module, une fonction ou un autre mot-clé intégré, le mode d'aide interactif ne fera que hausser ses épaules virtuelles.

(3) Pour quitter le mode d'aide interactif, tapez quit et appuyez sur la touche ENTRER.

(4) L’invite revient à >>> pour signaler que vous avez quitté le mode d'aide interactif et que vous êtes retourné dans le shell Python.

IDLE, le shell graphique Python, inclut également un éditeur de texte compatible à Python.

I-G - Les éditeurs Python et les IDE

IDLE n’est pas le seul outil possible pour écrire des programmes en Python. Bien qu’il soit utile pour apprendre le langage lui-même, beaucoup de développeurs lui préfèrent d’autres éditeurs de textes ou des « Environnements de Développement Intégrés » (IDE). Je ne vais pas les mentionner ici, mais la communauté Python maintient une liste des éditeurs compatibles Python qui couvre un large éventail de plateformes et de licences logicielles.

Vous pouvez aussi souhaiter vérifier la liste des IDE compatibles Python, bien que peu d’entre eux supportent Python 3 aujourd’hui. Un qui le fait est PyDev, un plugin pour Eclipse qui fait d’Eclipse un IDE Python complet. Tant Eclipse que Python sont multiplateformes et OpenSource.

Du côté commercial on trouve l’IDE Komodo de ActiveState.

Je programme en Python depuis neuf ans, et j’édite mes programmes Python dans GNU Emacs et les déboguent en ligne de commande dans le shell Python. Il n’y a pas de bonne ou de mauvaise manière de développer en Python. Trouvez la manière qui vous convient !

II - Chapitre 1 : Votre premier programme Python

II-A - Allons y !

Les conventions usuelles voudraient que je démarre ce tutoriel en vous ennuyant avec les éléments fondamentaux de la programmation afin que nous puissions travailler doucement pour construire quelque chose d’utile. Passons outre. Voici un exemple de programme Python complet et qui fonctionne. Il n’a probablement aucun sens pour vous. Ne vous inquiétez pas : on va le disséquer ligne par ligne. Mais commencez par le lire et voyez ce que vous pouvez en comprendre.

1. SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],

2. 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

4. def approximate_size(size, a_kilobyte_is_1024_bytes=True):

5. '''Convert a file size to human-readable form.

7. Keyword arguments:

8. size -- file size in bytes

9. a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024

10. if False, use multiples of 1000

12. Returns: string

14. '''

15. if size < 0:

16. raise ValueError('number must be non-negative')

18. multiple = 1024 if a_kilobyte_is_1024_bytes else 1000

19. for suffix in SUFFIXES[multiple]:

20. size /= multiple

21. if size < multiple:

22. return '{0:.1f} {1}'.format(size, suffix)

24. raise ValueError('number too large')

26. if __name__ == '__main__':

27. print(approximate_size(1000000000000, False))

28. print(approximate_size(1000000000000))

Le moment est venu d’exécuter ce programme depuis la ligne de commande. Sur Windows, vous devriez avoir quelque chose qui ressemble à ceci :

c:\home\diveintopython3\examples> c:\python31\python.exe humansize.py
1.0 TB
931.3 GiB

Sur Max OSX ou Linux, ça ressemblera plutôt à ceci :

you@localhost:~/diveintopython3/examples$ python3 humansize.py
1.0 TB
931.3 GiB

Que vient-il de se passer ? Vous avez exécuté votre premier programme Python. Vous avez appelé l’interpréteur Python depuis la ligne de commande et lui avez donné le nom du script que vous vouliez exécuter. Ce script définit une seule fonction, la fonction approximate_size(), qui à partir d’une taille exacte de fichier en octets (bytes) renvoie cette même taille sous une forme plus jolie (mais approximative). Vous avez déjà probablement vu cela avec Windows Explorer, ou avec Mac OS X Finder, ou encore avec Nautilus, Dolphin ou Thunar sous Linux. Si vous faites afficher un dossier de documents sous la forme de listes à plusieurs colonnes, alors apparaît une table avec l’icône du document, la taille, le type, la date de dernière modification, etc. Si le dossier contient un fichier de 1093 octets, votre gestionnaire de fichiers n’affichera pas 1093 octets, mais plutôt quelque chose comme 1Ko à la place. C’est ce que calcule la fonction approximate_size().

Regardez à la fin du script les deux appels à print(approximate_size(arguments)). Ce sont des appels de fonction. approximate_size() est d’abord appelée en lui passant un certain nombre d’arguments, puis la valeur de retour de cette fonction est elle-même directement passée en argument de la fonction print(). La fonction print() est une fonction intégrée à Python. Vous n’en verrez pas de déclaration explicite. Vous pouvez simplement l’utiliser n’importe quand, n’importe où. Il y a beaucoup d’autres fonctions intégrées (et beaucoup plus encore dans des modules séparés), mais patience…

Pourquoi lancer le script en ligne de commande vous renvoie toujours le même résultat ? On va voir cela. Mais d’abord, regardons de plus près la fonction approximate_size().

II-B - Déclarer une fonction

Python a des fonctions comme la plupart des autres langages, mais on n’a pas besoin d’avoir de fichier de prototypes (ou fichiers de déclarations, communément appelés entêtes ou headers) comme en C++ ou avec un sectionnement interface/implémentation comme en Pascal. Quand vous avez besoin d’une fonction, déclarez la simplement comme ceci :

def approximate_size(size, a_kilobyte_is_1024_bytes=True):

La déclaration de la fonction débute par le mot-clef def suivi du nom de la fonction et suivi d’arguments entre parenthèses. Les arguments sont séparés par des virgules.

Notez que la fonction ne définit pas le type de la valeur qu’elle renvoie. Les fonctions Python ne sont pas typées ; elles ne spécifient d’ailleurs même pas si elles retournent une valeur ou non. (En fait, toutes les fonctions Python retournent une valeur : si la fonction exécute une instruction return, la fonction renvoie cette valeur ; sinon, la fonction va retourner None qui est la valeur nulle ou vide de Python).

Dans certains langages, les fonctions (qui retournent une valeur) démarrent par function et les subroutines ou procédures (qui ne retournent pas de valeur) commencent par sub. Il n’y a pas de subroutines en Python. Tout est fonction et toutes les fonctions renvoient une valeur (même si elle vaut éventuellement None). Elles débutent toutes par le mot-clef def.

La fonction approximate_size() prend deux arguments – size et a_kilobyte_is_1024_bytes– mais aucun type n’est spécifié pour ceux-ci. En Python, les variables ne sont jamais explicitement typées. Python devine de quel type est une variable, et en garde la trace en interne.

En Java ou dans d’autres langages typés statiquement, vous devez spécifier le type des arguments d’une fonction ainsi que celui de sa valeur de retour. En Python, vous n’avez jamais à spécifier explicitement le type de quoi que ce soit. En se basant sur la valeur que vous assignez à une variable, Python garde une trace du type de la donnée en interne.

II-B-1 - Arguments nommés et optionnels

Python permet de construire des fonctions dont les arguments peuvent avoir des valeurs par défaut. Ainsi, si la fonction est appelée sans argument, les arguments prennent les valeurs par défaut. De plus les arguments peuvent être spécifiés dans n’importe quel ordre en utilisant les arguments nommés.

Regardons à nouveau la déclaration de la fonction approximate_size() :

def approximate_size(size, a_kilobyte_is_1024_bytes=True):

Le deuxième argument, a_kilobyte_is_1024_bytes, spécifie True comme valeur par défaut. Ceci signifie que l’argument est optionnel : vous pouvez appeler la fonction sans et Python agira comme si vous aviez mentionné True comme second paramètre.

Considérons à présent la fin du script

if __name__ == '__main__':
print(approximate_size(1000000000000, False))   ### (1)
print(approximate_size(1000000000000))          ### (2)

(1) Ceci appelle la fonction approximate_size() avec deux arguments. À l’intérieur de la fonction, a_kilobyte_is_1024_bytes vaut False étant donné que vous l’avez stipulé explicitement comme second argument.

(2) Ceci appelle la fonction approximate_size() avec un seul argument,ce qui ne pose pas de problème puisque le second argument est optionnel ! Étant donné que vous ne l’avez pas spécifié, il prend la valeur par défaut True définie dans la déclaration de la fonction.

Vous pouvez aussi passer des valeurs à une fonction en les nommant :

1. >>> from humansize import approximate_size

2. >>> approximate_size(4000, a_kilobyte_is_1024_bytes=False)       ### (1)

3. '4.0 KB'

4. >>> approximate_size(size=4000, a_kilobyte_is_1024_bytes=False)  ### (2)

5. '4.0 KB'

6. >>> approximate_size(a_kilobyte_is_1024_bytes=False, size=4000)  ### (3)

7. '4.0 KB'

8. >>> approximate_size(a_kilobyte_is_1024_bytes=False, 4000)       ### (4)

9. File "<stdin>", line 1

10. SyntaxError: non-keyword arg after keyword arg

11. >>> approximate_size(size=4000, False)                           ### (5)

12. File "<stdin>", line 1

13. SyntaxError: non-keyword arg after keyword arg

(1) Ceci appelle la fonction approximate_size() avec la valeur 4000 pour le premier argument (size) et False pour l’argument nommé a_kilobyte_is_1024_bytes (ici cela correspond au second argument, mais peu importe comme on va le voir dans un instant).

(2) Ceci appelle la fonction approximate_size() avec la valeur 4000 pour l’argument nommé size et False pour l’argument nommé a_kilobyte_is_1024_bytes (ici ces arguments nommés sont spécifiés dans le même ordre que dans la déclaration de la fonction, mais peu importe).

(3) Ceci appelle la fonction approximate_size() avec la valeur False pour l’argument nommé a_kilobyte_is_1024_bytes et la valeur 4000 pour l’argument nommé size (Voyez-vous ? Je vous avais dit que l’ordre n’importait pas lorsqu’on nomme les arguments !).

(4) Ceci provoque une erreur car vous ne pouvez pas appeler un argument nommé avant un argument non nommé (ou argument positionnel). On commence toujours par spécifier les arguments non nommés (là, l’ordre est important) puis les arguments nommés (ceux-là, dans l’ordre que l’on veut)

(5) Ceci provoque une erreur également pour la même raison que précédemment. Surprenant ? Après tout, vous passez 4000 à l’argument nommé size, donc « évidemment » que False est la valeur pour l’argument a_kilobyte_is_1024_bytes. Mais Python ne fonctionne pas comme cela. Dès que vous avez un argument nommé, tous les arguments suivants doivent eux aussi être nommés.

II-C - Écrire du code lisible : la  docstring

Je ne veux pas vous ennuyer avec un long sermon moralisateur sur l’importance de la documentation du code. Sachez juste qu’un script est écrit une seule fois, mais lu de nombreuses fois et l’audience la plus importante pour votre code est vous-même, 6 mois après l’avoir écrit (c’est-à-dire après avoir tout oublié de lui, mais quand vous avez besoin de vous remettre dedans pour corriger un bogue). Avec Python, c’est facile d’écrire du code lisible, donc tirez-en parti. Vous me remercierez dans 6 mois.

Vous pouvez documenter une fonction Python en lui attribuant une docstring (raccourci de l’anglais documentation string, le mot string signifiant chaîne de caractères). Dans ce programme, la fonction approximate_size() a une docstring :

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
'''Convert a file size to human-readable form.

Keyword arguments:
size -- file size in bytes
a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
if False, use multiples of 1000

Returns: string

'''

Toute fonction mérite une bonne docstring.

Les triples apostrophes indiquent une chaîne de caractères sur plusieurs lignes. Tout ce qui se trouve entre le début et la fin des apostrophes triples constitue une seule et unique chaîne de caractères, y compris les retours à la ligne, les espaces et aussi les éventuels guillemets et apostrophes. Vous pouvez l’utiliser n’importe où, mais vous verrez cela le plus souvent pour définir une docstring.

Les triples apostrophes sont aussi un moyen facile de définir une chaîne de caractères possédant à la fois des apostrophes simples et doubles, comme on le ferait avec qq/…/ en Perl 5.

Tout ce qui est entre les triples apostrophes constitue la docstring de la fonction qui renseigne sur ce que la fonction fait. Une docstring, si elle existe, doit être la première chose définie dans une fonction (c’est-à-dire, sur la ligne qui suit la déclaration de la fonction). Techniquement, vous n’avez pas besoin de fournir une docstring à votre fonction, mais vous devriez toujours le faire. Je sais que vous avez entendu cela dans tous les cours de programmation que vous avez suivis, mais Python vous donne une motivation supplémentaire : la docstring est disponible lors de l’exécution comme un attribut de la fonction.

Beaucoup d'environnements de développement (IDE) Python utilisent les  docstrings pour fournir une documentation contextuelle. Ainsi lorsque vous tapez le nom d’une fonction, sa docstring apparaît en info-bulle.

II-D - La recherche du chemin d’import

Avant d’aller plus loin, faisons brièvement mention de la recherche des bibliothèques. Python regarde à plusieurs endroits lorsque vous essayez d’importer un module. En particulier, il regarde dans tous les répertoires définis dans sys.path. Ce n’est qu’une liste et vous pouvez facilement l'afficher ou la modifier avec les méthodes habituelles sur les listes. (Vous en apprendrez plus dans le prochain chapitre.)

>>> import sys                                                 (1)
>>> sys.path                                                   (2)
['',
'/usr/lib/python31.zip',
'/usr/lib/python3.1',
'/usr/lib/python3.1/plat-linux2@EXTRAMACHDEPPATH@',
'/usr/lib/python3.1/lib-dynload',
'/usr/lib/python3.1/dist-packages',
'/usr/local/lib/python3.1/dist-packages']
>>> sys                                                        (3)
<module 'sys' (built-in)>
>>> sys.path.insert(0, '/home/mark/diveintopython3/examples')  (4)
>>> sys.path                                                   (5)
['/home/mark/diveintopython3/examples',
'',
'/usr/lib/python31.zip',
'/usr/lib/python3.1',
'/usr/lib/python3.1/plat-linux2@EXTRAMACHDEPPATH@',
'/usr/lib/python3.1/lib-dynload',
'/usr/lib/python3.1/dist-packages',
'/usr/local/lib/python3.1/dist-packages']

(1) On importe le module sys pour avoir accès à toutes ses fonctions et attributs.

(2) sys.path est une liste de noms de répertoires dans laquelle Python recherche les bibliothèques. (Vous avez probablement quelque chose de différent ; cela dépend de votre système d’exploitation, de votre version de Python et de son installation d’origine). Python recherche dans ces répertoires (dans l’ordre) un fichier .py dont le nom correspond à ce que vous essayez d’importer.

(3) En fait, j'ai un peu simplifié : c’est un peu plus compliqué que cela car tous les modules ne sont pas des fichiers .py. Certains sont des modules intégrés (built-in modules) ; ils font partie de Python lui-même. Les modules intégrés se comportent comme les autres modules, mais leur code source n’est pas disponible car il n’est pas écrit en Python ! (Comme Python lui-même, ces modules intégrés sont écrits en C).

(4) Vous pouvez ajouter un nouveau répertoire au sys.path et ainsi, Python regardera aussi dans ce répertoire lorsque vous tenterez d’importer un module. L’effet perdure aussi longtemps que Python continue à tourner.

(5) En utilisant sys.path.insert(0, new_path), vous insérez un nouveau répertoire en tête de la liste sys.path, c'est-à-dire au début de la liste des chemins où Python va rechercher des modules. La plupart du temps c’est ce que vous voudrez faire. En cas de conflit de nommage (par exemple, si Python est livré avec la version 2 d’une bibliothèque particulière mais que vous souhaitez utiliser la version 3), ceci assure que vos modules seront trouvés et utilisés au lieu des modules qui ont été installés en même temps que Python.

II-E - Tout est objet

Au cas où vous auriez oublié, je rappelle que les fonctions Python ont des attributs et que ces attributs sont disponibles au moment de l’exécution. Une fonction, comme n’importe quoi d’autre en Python, est un objet.

1. >>> import humansize                               ##  (1)

2. >>> print(humansize.approximate_size(4096, True))  ##  (2)

3. 4.0 KiB

4. >>> print(humansize.approximate_size.__doc__)      ##  (3)

5. Convert a file size to human-readable form.

7. Keyword arguments:

8. size -- file size in bytes

9. a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024

10. if False, use multiples of 1000

12. Returns: string

(1) La première ligne importe le programme humansizecomme un module — un bout de code que vous pouvez utiliser interactivement, ou dans un programme Python plus gros. Une fois que vous avez importé ce module, vous pouvez faire référence à toutes ses fonctions, classes ou attributs publics et vous pouvez aussi le faire en interactif dans le terminal Python. C’est un concept important et vous en verrez beaucoup dans ce livre.

(2) Quand vous voulez utiliser une fonction définie dans un module importé, vous devez inclure le nom du module. Donc vous ne pouvez pas simplement écrire approximate_size, mais vous devez écrire humansize.approximate_size. Si vous utilisez des classes en Java, ceci devrait vous être vaguement familier.

(3) Au lieu d’appeler la fonction comme vous vous y seriez attendu, vous demandez l’attribut __doc__ de la fonction.

import en Python agit comme require en Perl. Une fois que vous avez importé un module, vous accédez à ses fonctions avec  module.fonction en Python, et avec module::fonction en Perl.

Absolument tout dans Python est objet et tout objet peut avoir des attributs et des méthodes. Toute fonction a un attribut __doc__ qui retourne sa docstring définie dans le code source de la fonction. Le module sys est un objet qui a (entre autres choses) un attribut nommé path. Et ainsi de suite.

Cependant, ceci ne répond pas à la question la plus fondamentale : qu’est-ce qu’un objet ? Les langages de programmation définissent l’ « objet » de différentes manières. Dans certains langages, tout objet doit avoir des attributs et des méthodes. Dans d’autres, tout objet est dérivable (c'est-à-dire que l’on peut créer une sous-classe de l’objet). En Python la définition est plus vague. Certains objets n’ont ni attribut ni méthode, mais ils pourraient en avoir. Tous les objets ne sont pas dérivables. Cependant tout est un objet dans le sens où l’on peut l’assigner à une variable ou le passer comme argument à une fonction.

Vous avez peut-être entendu le terme « objet de première classe » (first-class object) dans d’autres contextes de programmation. En Python, les fonctions sont des objets de première classe. Vous pouvez passer une fonction en argument à une autre fonction. Les modules sont également des objets de première classe. Vous pouvez passer un module entier en argument à une fonction. Les classes sont des objets de première classe et les instances individuelles d’une classe sont aussi des objets de première classe.

Ceci est important, donc je vais le répéter au cas où vous l’oublieriez : en Python, tout est objet. Les chaînes de caractères sont des objets. Les listes sont des objets. Les fonctions sont des objets. Les classes sont des objets. Les instances de classe sont des objets. Tout module est un objet.

II-F - Indenter son code

Les fonctions Python n’ont pas de mot-clef begin ou end explicite et pas non plus d’accolades pour marquer où démarre la fonction et où elle s’arrête. Le seul délimiteur est un caractère deux-points ( : ) et l’indentation du code lui-même.

1. def approximate_size(size, a_kilobyte_is_1024_bytes=True):  ## (1)

2. if size < 0:                                            ## (2)

3. raise ValueError('number must be non-negative')     ## (3)

4. ## (4)

5. multiple = 1024 if a_kilobyte_is_1024_bytes else 1000

6. for suffix in SUFFIXES[multiple]:                       ## (5)

7. size /= multiple

8. if size < multiple:

9. return '{0:.1f} {1}'.format(size, suffix)

11. raise ValueError('number too large')

(1) Les blocs de code sont définis via leur indentation. Par « bloc de code », j’entends les fonctions, les conditions if, les boucles for et while, etc. Une indentation marque le début d’un bloc, la fin de cette indentation marque la fin du bloc. Il n’y a pas d’accolades explicites, de parenthèses ou de mots-clefs. Ceci signifie que les espaces sont importants et doivent être cohérents. Dans cet exemple, le code de la fonction est indenté avec quatre espaces. Ce n’est pas obligatoirement 4 espaces, il faut juste que les espaces soient cohérents. La première ligne qui n’est pas indentée marque la fin du bloc ou de la fonction.

(2) En Python, une instruction ifest suivie d’un bloc de code. Si l’expression du if est évaluée à True, le bloc indenté est exécuté, sinon on passe directement au bloc else (s’il y en a un). Notez qu’il n’y a pas de parenthèses autour de l’expression.

(3) La ligne est à l’intérieur du bloc if. L’instruction raise va lancer une exception (du type ValueError) si size < 0.

(4) Ce n’est pas la fin de la fonction. Les lignes entièrement blanches ne comptent pas. Elles peuvent simplement rendre le code plus lisible et elles ne sont pas considérées comme un délimiteur de bloc de code. La fonction continue sur la ligne suivante.

(5) La boucle for marque également le début d’un bloc de code. Les blocs de code peuvent contenir de multiples lignes tant qu’elles sont indentées de la même manière. Cette boucle for contient trois lignes dans son bloc. Il n’y a pas d’autre syntaxe spécifique pour les blocs de codes sur plusieurs lignes. Vous n’avez qu’à indenter et c’est tout.

Après quelques protestations et analogies méprisantes avec le Fortran, vous ferez la paix avec cela et commencerez à voir l'intérêt de l'indentation. L’un des principaux avantages est que tous les programmes Python se ressemblent, car l’indentation est nécessaire au langage et n’est pas juste une question de style. Ceci rend plus facile la lecture et la compréhension de code Python écrit par d’autres personnes.

Python utilise le retour de chariot (le passage à la ligne) pour séparer les instructions et le caractère deux points et l’indentation pour séparer les blocs. C++ et Java utilisent des points virgules pour séparer les instructions et des accolades pour séparer les blocs.,

II-G - Les exceptions

Les exceptions sont partout en Python. Pratiquement tous les modules de la bibliothèque standard de Python les utilisent et Python lui-même en lance dans beaucoup de circonstances. Nous le verrons à de nombreuses reprises dans ce livre.

Qu’est-ce qu’une exception ? Souvent, c’est une erreur, une indication que quelque chose se passe mal. (Toutes les exceptions ne sont pas des erreurs, mais laissons ceci de côté pour l’instant.) Certains langages de programmation encouragent l’utilisation des codes d’erreur de sortie, que vous pouvez vérifier. Python encourage l’utilisation des exceptions que vous pouvez traiter.

Quand une erreur survient dans le shell Python, il affiche certains détails sur l’exception et comment elle est arrivée. Ceci s'appelle une exception non-gérée. Quand une exception est lancée, il n’y a pas de code pour explicitement l’identifier et la traiter, donc elle remonte jusqu’au haut niveau du shell Python, qui vous affiche des informations de débogage et s'arrête. Une erreur dans le shell ce n’est pas très grave, mais si cela arrive lorsque votre programme Python actuel tourne, le programme entier s’arrêtera abruptement si rien ne traite l’exception. Peut-être que c’est ce que vous voulez, ou peut-être pas.

Contrairement à Java, les fonctions Python ne déclarent pas quelles exceptions elles peuvent lancer. C’est à vous de déterminer quelles sont les exceptions que vous avez besoin d'intercepter.

Une exception n’a cependant pas besoin de provoquer un arrêt complet du programme. Les exceptions peuvent être traitées. Parfois une exception est réelle car vous avez un bogue dans votre code (comme accéder à une variable qui n’existe pas), mais parfois une exception est juste quelque chose que vous pouvez anticiper. Si vous ouvrez un fichier, il peut ne pas exister. Si vous importez un module, il peut ne pas être installé. Si vous vous connectez à une base de données, elle peut être indisponible ou vous pouvez ne pas avoir les droits suffisants pour y accéder. Si vous connaissez une ligne de code qui peut lancer une exception, vous pouvez traiter l’exception en utilisant le bloc try…except.

Python utilise les blocs try…except pour traiter les exceptions et l’instruction raise pour la lancer. Java et C++ utilisent les blocs try…catch pour traiter les exceptions et l’instruction throw pour les générer.

La fonction approximate_size() lance une exception dans deux cas de figure différents : si la taille (size) donnée est plus grande que ce que la fonction est capable de traiter ou bien si elle est inférieure à zéro.

if size < 0:
raise ValueError('number must be non-negative')

La syntaxe pour lancer une exception est assez simple. On utilise le mot-clef raise, suivi du nom de l’exception et optionnellement d’une chaîne de caractères lisible pour un humain (une phrase) pour faciliter le débogage. La syntaxe est évocatrice d’un appel de fonction. (En réalité, les exceptions sont implémentées comme des classes et le mot-clef raise crée ici une instance de la classe ValueError et passe la chaîne de caractères « number must be non-negative » à sa méthode d’initialisation. Mais nous verrons cela plus tard !)

Vous n’avez pas besoin de gérer l’exception dans la fonction qui la lance. Si une fonction ne gère pas une exception, alors l’exception est renvoyée à la fonction appelante, qui la renvoie elle-même à sa fonction appelante et ainsi de suite. L’erreur « remonte la pile ». Si une exception n’est jamais gérée, le programme plante et Python affichera un traceback sur la sortie standard et le programme s’arrêtera là. A nouveau, ce n’est peut-être pas ce que vous voulez ; tout dépend de ce que votre programme fait.

II-G-1 - Intercepter les erreurs d'importation de module

L’une des exceptions intégrées à Python est l’ImportError, qui est lancée lorsque vous tentez d’importer un module et que cela échoue. Cela peut arriver pour toute une panoplie de raisons, mais, dans le cas le plus simple, c’est lorsque votre module n’existe pas dans votre sys.path. Vous pouvez utiliser cela pour inclure des fonctionnalités optionnelles à votre programme. Par exemple, la librairie chardet fournit un encodeur de chaîne de caractères avec détection automatique. Peut-être que votre programme veut utiliser cette librairie (si elle existe), mais peut-être que vous pourriez vouloir continuer également même si l’utilisateur ne dispose pas de cette librairie. On peut alors faire ceci avec un bloc try…except.

try:
import chardet
except ImportError:
chardet = None

Plus tard dans votre code, vous pourrez vérifier la présence ou non du module chardet avec un simple test conditionnel if .

if chardet:
# faire quelque chose
else:
# continuer tout de même

Un autre cas commun d’utilisation de l’exception ImportError est quand deux modules implémentent une API commune, mais que l’une est préférable (Peut-être est-elle plus rapide ou utilise-t-elle moins de mémoire). Vous pouvez essayer d’importer un module et si cela échoue, vous rabattre sur le second module. Par exemple, dans le chapitre dédié à l’XML, deux modules implémentant une même API – nommée ElementTree – sont présentés. Le premier lxml est un module tiers que l’on a besoin de télécharger et d’installer soi-même. Le second, xml.etree.ElementTree, est plus lent mais il est intégré à la bibliothèque standard de Python 3.

try:
from lxml import etree
except ImportError:
import xml.etree.ElementTree as etree

À la fin du bloc try…except, vous avez importé un module et il se nomme etree. Comme les deux modules implémentent une API commune, le reste de votre code n’a pas besoin de vérifier quel module a été importé. Et comme le module a été importé sous le nom etree dans les deux cas, le reste de votre code n’a plus à être parsemé de clauses if pour appeler des modules aux noms différents.

II-H - Variables non initialisées

Regardons de plus près la ligne de code suivante de la fonction approximate_size() :

multiple = 1024 if a_kilobyte_is_1024_bytes else 1000

Notez que nous n’avons jamais déclaré la variable multiple, nous lui avons simplement affecté une valeur. Cela suffit, car Python vous le permet. Ce que Python ne vous permettra pas, c’est une référence à une variable qui n’a pas été affectée à une valeur auparavant. Si vous le faites, cela lancera une exception NameError.

>>> x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>> x = 1
>>> x
1

Vous remercierez Python un jour pour cela.

II-I - Tout est sensible à la casse

Tous les noms dans Python sont sensibles à la casse (c'est-à-dire à la distinction entre lettres minuscules et lettres capitales) : les noms des variables, les noms des fonctions, les noms des classes, les noms des modules, les noms des exceptions. Si c'est quelque chose que vous pouvez obtenir, définir, appeler, construire, importer ou lancer, c’est sensible à la casse.

1. >>> an_integer = 1

2. >>> an_integer

3. 1

4. >>> AN_INTEGER

5. Traceback (most recent call last):

6. File "<stdin>", line 1, in <module>

7. NameError: name 'AN_INTEGER' is not defined

8. >>> An_Integer

9. Traceback (most recent call last):

10. File "<stdin>", line 1, in <module>

11. NameError: name 'An_Integer' is not defined

12. >>> an_inteGer

13. Traceback (most recent call last):

14. File "<stdin>", line 1, in <module>

15. NameError: name 'an_inteGer' is not defined

II-J - Lancer un script

Souvenez-vous : en Python tout est objet.

Les modules Python sont des objets et ils ont des attributs bien pratiques. Vous pouvez les utiliser pour tester facilement vos modules au fur et à mesure que vous les écrivez, en incluant un bloc spécial de code qui s’exécute lorsque vous lancez le fichier Python en ligne de commande. Prenons les dernières lignes de humansize.py :

if __name__ == '__main__':
print(approximate_size(1000000000000, False))
print(approximate_size(1000000000000))

Comme en C, Python utilise == pour les comparaisons et = pour l’affectation. Contrairement au C, Python ne supporte pas l’affectation en ligne, donc il n'y a aucune chance d'attribuer accidentellement la valeur que vous pensiez comparer.

Qu'y a-t-il de spécial dans cette condition if ? Et bien, les modules sont des objets et les modules ont un attribut intégré, __name__. Le __name__ d’un module dépend de la manière dont vous utilisez le module. Si vous importez le module, alors __name__ est le nom du fichier du module sans le chemin ni l’extension.

>>> import humansize
>>> humansize.__name__
'humansize'

Mais vous pouvez aussi exécuter votre module directement en tant que programme autonome. Dans ce cas __name__ aura la valeur spéciale __main__. Ici, Python évaluera alors la condition if, identifiera un test renvoyant vrai et exécutera le code du bloc if, qui dans notre cas, imprime deux valeurs à l’écran.

c:\home\diveintopython3> c:\python31\python.exe humansize.py
1.0 TB
931.3 GiB

Mais si vous appelez ce script comme un module, la partie contrôlée par la condition if __name__ == '__main:_ ne s'exécutera pas et la fonction renverra l'approximation de la taille qui lui est passée en paramètre.

Et voici votre premier programme Python !

II-K - Pour aller plus loin (en anglais)

- PEP 257: Docstring Conventions explique ce qui différencie une bonne docstring d’une excellente docstring.

- Python Tutorial: Documentation Strings traite également du même sujet.

- PEP 8: Style Guide for Python Code aborde les bonnes pratiques sur les styles d’indentation.

- Python Reference Manual explique ce que veut dire “En Python, tout est objet”, car certaines personnes pédantes aiment discuter longuement de ce genre de choses.

III - Chapitre 2 – Les types de données natifs

- « L’émerveillement est le fondement de toute philosophie, l’investigation assure son progrès, l’ignorance entraîne sa fin. » Michel de Montaigne

III-A - Plongeons

Les types de données : Mettez un instant de côté votre premier programme Python, et parlons un peu types de données. En Python, toute valeur a un type, mais vous n’êtes pas obligé de déclarer le type des variables. Comment est-ce possible ? En se basant sur l’affectation initiale d’une valeur à une variable, Python déduit le type de la variable et en garde trace en interne.

Python a de nombreux types de données natifs. Voici les plus importants :

1. Les booléens (booleans) sont vrais ou faux (valeurs True ou False).

2. Les nombres (numbers) peuvent être des entiers (1 et 2), des nombres à virgule flottante (1.1 et 1.3) des fractions (1/2 et 2/3) ou même des nombres complexes.

3. Les chaînes (strings en anglais et en Python) sont des séquences de caractères Unicode, par exemple un document HTML.

4. Les octets et les tableaux d’octets représentent par exemple une image JPEG.

5. Les listes sont des séquences ordonnées de valeurs.

6. Les tuples sont des séquences ordonnées mais immuables de valeurs.

7. Les sets ou ensembles sont des conteneurs de valeurs en vrac.

8. Les dictionnaires sont des ensembles non ordonnés de paires clé-valeur.

Il y a bien entendu plus de types que cela. Tout est objet en Python, ainsi y a-t-il des types comme module, function, class, method, file (fichier), et même compiled code (code compilé). Vous en avez déjà rencontré certains : les modules ont des noms, les fonctions ont des docstrings, etc. Vous en apprendrez davantage sur les classes dans le chapitre Classes et itérateurs, et à propos des fichiers dans le chapitre Fichiers.

Les chaînes et les octets sont suffisamment importants et compliqués pour avoir droit à des chapitres dédiés. Voyons d’abord les autres.

III-B - Les booléens

Vous pouvez utiliser pour ainsi dire n’importe quelle expression dans un contexte booléen.

Un booléen est soit vrai, soit faux. Python possède deux constantes, astucieusement dénommées True (vrai) et False (faux), qui peuvent être utilisées pour assigner directement des valeurs booléennes. Des expressions peuvent également être évaluées comme des valeurs booléennes. À certains endroits (comme dans les instructions if), Python s’attend à ce qu’une expression soit évaluée comme une valeur booléenne. Ces endroits sont appelés des contextes booléens. Vous pouvez utiliser pour ainsi dire n’importe quelle expression dans un contexte booléen, et Python essaiera de déterminer si sa valeur est vraie. Les différents types de données ont des règles différentes pour déterminer quelles valeurs sont vraies ou fausses dans un contexte booléen. (Cela vous paraîtra plus clair quand vous aurez vu quelques exemples concrets plus loin dans ce chapitre.)

Comme exemple, prenez ce bout de code de humansize.py :

If size<0 :

raise ValueError(‘le nombre ne doit pas être négatif’)

size est un entier, 0 est un entier, et < est un opérateur numérique. Le résultat de l’expression size < 0 est toujours un booléen. Vous pouvez vérifier cela vous-même dans le shell interactif de Python :

>>> size=1
>>> size<0
False
>>> size=0
>>> size<0
False
>>> size=-1
>>> size<0
True

Suite à un héritage laissé par Python 2, les booléens peuvent être manipulés comme des nombres. True correspond à 1 ; False correspond à 0.

1. >>> True + True

2. 2

3. >>> True - False

4. 1

5. >>> True * False

6. 0

7. >>> True / False

8. Traceback (most recent call last):

9. File "<stdin>", line 1, in <module>

10. ZeroDivisionError: int division or modulo by zero

Aïe, aïe, aïe ! Ne faites jamais ça ! Oubliez que j’y ai même seulement fait allusion !

III-C - Les nombres

Les nombres sont impressionnants. Il y a un tel choix ! Python supporte à la fois les entiers et les nombres en virgule flottante. Et il n’y a pas de déclaration de type pour les distinguer ; Python fait la différence en fonction de la présence ou de l’absence du point décimal.

>>> type(1)            (1)
<class 'int'>
>>> isinstance(1,int)    (2)
True
>>> 1+1            (3)
2
>>> 1+1.0            (4)
2.0
>>> type(2.0)
<class 'float'>

NdlR : les notations (1), (2), etc. ne font pas partie du code.

(1) Vous pouvez utiliser la fonction type() pour vérifier le type de n’importe quelle valeur ou variable. Comme vous vous en doutiez, 1 est un entier (int).

(2) De même, vous pouvez utiliser la fonction isinstance() pour vérifier si une valeur ou une variable est d’un type donné.

(3) Ajouter un entier (int) à un entier donne un entier.

(4) Ajouter un entier (int) à un flottant (float) donne un flottant. Python force la conversion de l'entier en flottant, puis retourne le résultat de l'addition dans un flottant.

III-C-1 - Convertir des entiers en flottants et vice-versa

Comme vous venez de le voir, certains opérateurs (comme l’addition) vont convertir les entiers en flottants selon les besoins. Vous pouvez aussi les convertir vous-même.

>>> float(2)            (1)
2.0
>>> int(2.0)            (2)
2
>>> int(2.5)            (3)
2
>>> int(-2.5)            (4)
-2
>>> 1.12345678901234567890    (5)
1.1234567890123457
>>> type(1000000000000000)    (6)
<class 'int'>

(1) Vous pouvez explicitement convertir un entier en un flottant avec la fonction float().

(2) Sans surprise, vous pouvez aussi convertir un flottant en un entier avec la fonction int().

(3) La fonction int() va tronquer le nombre et non l’arrondir.

(4) La fonction int() tronque les nombres négatifs en allant vers 0. Il s’agit d’une vraie fonction de troncature, non d’une mise à zéro de la partie décimale.

(5) Les nombres à virgule flottante ont une précision jusqu’à la quinzième décimale.

1. Les entiers peuvent être arbitrairement grands.

Python 2 possédait deux types d’entiers séparés int et long. Le type int était limité à . Python 3 ne possède qu’un seul type d’entier qui se comporte essentiellement comme le type long de Python 2. Pour plus de précisions, voir PEP 237.

III-C-2 - Opérations numériques courantes

Vous pouvez faire toutes sortes de choses avec les nombres.

>>> 11/2        (1)
5.5
>>> 11//2        (2)
5
>>> -11//2        (3)
-6
>>> 11.0//2        (4)
5.0
>>> 11**2        (5)
121
>>> 11%2        (6)
1

(1) L’opérateur / effectue des divisions en virgule flottante. Même si le numérateur et le dénominateur sont des entiers, le résultat sera un nombre en virgule flottante (float).

(2) L’opérateur // effectue des divisions entières un peu particulières. Quand le résultat est positif, vous pouvez le considérer comme une troncature (non un arrondi) à 0 décimales, mais considérez ceci avec prudence.

(3) En effectuant une division entière d’un entier négatif, l’opérateur // arrondit par excès au plus proche entier. Mathématiquement parlant, il arrondit par défaut étant donné que -6 est plus petit que -5, mais si vous attendez un arrondi à -5 cela pourrait vous désarçonner.

(4) L’opérateur // ne retourne pas toujours un entier. Si soit le numérateur, soit le dénominateur est un flottant (float), il arrondit quand même à l’entier le plus proche, mais la valeur retournée sera un flottant.

(5) L’opérateur ** signifie « élevé à la puissance ». 11^2 donne 121.

(6) L’opérateur % retourne le reste de la division entière. 11 divisé par 2 est égal à 5 reste 1. Le résultat est donc 1.

En Python 2, l’opérateur / correspondait à une division entière. Mais vous pouviez en changer le comportement de façon à obtenir une division en virgule flottante en incluant une directive spéciale dans le code source. Dans Python 3, l’opérateur correspond toujours à une division en virgule flottante. Voir PEP 238 pour plus de détails.

III-C-3 - Fractions

Python n’est pas limité aux nombres entiers et aux nombres en virgule flottante. Il peut aussi faire toutes les mathématiques imaginables que vous avez apprises au lycée et que vous vous êtes promptement empressé d’oublier.

>>> import fractions            (1)
>>> x=fractions.Fraction(1,3)          (2)
>>> x
Fraction(1, 3)
>>> x*2                    (3)
Fraction(2, 3)
>>> fractions.Fraction(6,4)        (4)
Fraction(3, 2)
>>> fractions.Fraction(0,0)        (5)
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
fractions.Fraction(0,0)
File "C:\Users\ercamos\AppData\Local\Programs\Python\Python35\lib\fractions.py", line 186, in __new__
raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
ZeroDivisionError: Fraction(0, 0)

(1) Pour utiliser les fractions, commencez par importer le module fractions.

(2) Pour définir une fraction, créez un objet Fraction en lui passant le numérateur et le dénominateur comme arguments.

(3) Vous pouvez effectuer toutes les opérations mathématiques usuelles avec les fractions. Les opérations retournent un nouvel objet de type Fraction : 2*(1/3)=(2/3).

(4) L’objet Fraction va automatiquement simplifier les fractions : (6/4)=(3/2).

(5) Python a le bon goût de ne pas créer de fraction avec le dénominateur zéro.

III-C-4 - Trigonométrie

Vous pouvez aussi faire de la trigonométrie de base avec Python.

>>> import math
>>> math.pi                (1)
3.141592653589793
>>> math.sin(math.pi/2)          (2)
1.0
>>> math.tan(math.pi/4)          (3)
0.9999999999999999

(1) Le module math contient la constante π, qui est le rapport de la circonférence d’un cercle à son diamètre.

(2) Le module math contient toutes les fonctions trigonométriques de base, y compris sin(), cos(), tan(), ainsi que des variantes comme asin().

(3) Notez toutefois que Python n’est pas d’une précision infinie, tan(π/4) devrait retourner 1.0 et non 0.9999999999999999.

III-C-5 - Les nombres dans un contexte booléen.

Les valeurs à zéro sont False, toutes les autres valeurs sont True.

Vous pouvez utiliser des nombres dans un contexte booléen, par exemple dans une instruction if. Les valeurs zéro valent False (faux) et les valeurs différentes de zéro valent True (vrai).

>>> def est_ce_vrai(quelquechose):            (1)
if quelquechose:
print("oui,c'est vrai")
else:
print("non, c'est faux")

>>> est_ce_vrai(1)                    (2)
oui,c'est vrai
>>> est_ce_vrai(-1)
oui,c'est vrai
>>> est_ce_vrai(0)
non, c'est faux
>>> est_ce_vrai(0.1)                    (3)
oui,c'est vrai
>>> est_ce_vrai(0.0)
non, c'est faux
>>> import fractions
>>> est_ce_vrai(fractions.Fraction(1,2))        (4)
oui,c'est vrai
>>> est_ce_vrai(fractions.Fraction(0,1))
non, c'est faux

(1) Saviez-vous que vous pouviez définir vos propres fonctions dans le shell interactif de Python ? Appuyez juste sur ENTRÉE à la fin de chaque ligne et deux fois sur ENTRÉE à la fin de la fonction.

(2) Dans un contexte booléen, les valeurs non nulles sont vraies et les valeurs nulles sont fausses.

(3) Les nombres à virgule flottante non nuls sont vrais, 0.0 est faux. Méfiez-vous de ce dernier ! La moindre erreur d’arrondi (ce qui n’est pas du domaine de l’impossible, comme nous l’avons vu dans la section précédente) fera par exemple tester à Python la valeur 0,000000001 au lieu de 0 et il retournera True.

(4) Les fractions aussi peuvent être utilisées dans un contexte booléen. Fraction(0,n) sera faux pour toutes les valeurs de n (sauf 0 bien sûr). Toutes les autres fractions sont vraies.

III-D - Les Listes

Les listes sont les types de données à tout faire de Python. Quand je dis « liste », vous pensez peut-être « tableaux dont je dois définir la taille par avance et qui ne peuvent contenir que des éléments de type identique ». Ne pensez pas ça. Les listes sont bien plus cool que ça.

Une liste en Python est comme un tableau en Perl 5. Sauf qu’en Perl 5 les variables qui contiennent des tableaux commencent par le caractère @, alors qu’en Python les variables peuvent être désignées par un nom quelconque, Python se chargeant de garder en mémoire le type de la variable.

Une liste en Python est bien plus qu’un tableau en Java (bien qu’elle puisse être utilisée en tant que telle si c’est tout ce que vous attendez de la vie). Une meilleure analogie serait la classe ArrayList, qui peut contenir des objets arbitraires et peut s’agrandir dynamiquement au fur et à mesure qu’on y ajoute des éléments.

III-D-1 - Créer une liste

Il est facile de créer une liste : utilisez des crochets pour encadrer une liste de valeurs séparées par des virgules.

>>> a_list=['a','b','mpilgrim','z','example']    (1)
>>> a_list
['a', 'b', 'mpilgrim', 'z', 'example']
>>> a_list[0]                        (2)
'a'
>>> a_list[4]                        (3)
'example'
>>> a_list[-1]                        (4)
'example'
>>> a_list[-3]                        (5)
'mpilgrim'

(1) Tout d’abord, définissez une liste de 5 éléments. Notez que ceux-ci restent dans le même ordre. Une liste est un ensemble ordonné d’éléments.

(2) Une liste peut être utilisée comme un tableau de base 0. Le premier élément de toute liste non vide est toujours a_list[0].

(3) Le dernier élément de cette liste de cinq éléments est a_list[4], parce que les listes sont toujours à base 0.

(4) Un index négatif accède aux éléments à partir de la fin de la liste en comptant à l’envers. Le dernier élément de toute liste non vide est toujours a_list[-1].

(5) Si les index négatifs vous semblent déroutants, pensez-y de la manière suivante : a_list[-1]==a_list[len(a_list)-n]. Donc a_list[-3]==a_list[5-3]==a_list[2].

III-D-2 - Découper une liste

Une fois que vous avez défini une liste, vous pouvez en extraire n’importe quelle partie en tant que nouvelle liste. Cela s’appelle découper (slicing en anglais) une liste.

>>> a_list
['a', 'b', 'mpilgrim', 'z', 'example']
>>> a_list[1:3]            (1)
['b', 'mpilgrim']
>>> a_list[1:-1]            (2)
['b', 'mpilgrim', 'z']
>>> a_list[0:3]            (3)
['a', 'b', 'mpilgrim']
>>> a_list[:3]            (4)
['a', 'b', 'mpilgrim']
>>> a_list[3:]            (5)
['z', 'example']
>>> a_list[:]            (6)
['a', 'b', 'mpilgrim', 'z', 'example']

(1) Vous pouvez extraire une tranche de liste, appelée « slice », en spécifiant deux index. La valeur retournée est une nouvelle liste contenant tous les éléments de la liste, dans l’ordre, en commençant par l’élément ayant le premier index de la tranche (dans notre cas a_list[1]), et finissant par l’élément précédant celui ayant le deuxième index de la tranche (dans notre cas a_list[3], non compris).

(2) Le découpage fonctionne aussi si l’un ou les deux index sont négatifs. Au besoin vous pouvez vous représenter les choses de la façon suivante : en parcourant la liste de gauche à droite, le premier index de la tranche désigne le premier élément dont vous voulez, et le deuxième index de la tranche identifie le premier élément dont vous ne voulez pas. La valeur retournée correspond à tout ce dont vous avez voulu en cours de route.

(3) Les listes sont basées sur 0, ainsi a_list[0,3] renvoie les trois premiers éléments de la liste, en commençant par a_list[0] et en allant jusqu’à a_list[3], mais sans inclure ce dernier.

(4) Si l’index gauche de la tranche est égal à 0, vous pouvez l’omettre et il sera inclus implicitement. Ainsi a_list[:3] est équivalent à a_list[0:3], le 0 étant implicite.

(5) De même, si l’index de droite de la tranche correspond à la longueur de la liste, vous pouvez l’omettre. Ainsi a_list[3:] est équivalent à a_list[3:5], parce que cette liste a 5 éléments. Nous observons ici une amusante symétrie. Dans cette liste de 5 éléments, a_List[:3] retourne les 3 premiers éléments, et a_list[3:] retourne les 2 derniers. En fait a_list[:n] retourne toujours les n premiers éléments et a_list[n:] retourne le reste de la liste, quelle que soit la longueur de la liste.

(6) Si on omet les deux index, tous les éléments de la liste sont inclus. Mais cette liste n’est pas la même que celle du a_list original. Il s’agit d’une nouvelle liste qui contient tous les éléments de l’originale. a_list[:] est un raccourci pour obtenir une copie complète de la liste a_list.

III-D-3 - Ajouter des éléments à une liste

Il y a quatre façons d’ajouter des éléments à une liste.

>>> a_list=['a']
>>> a_list=a_list+[2.0,3]        (1)
>>> a_list                    (2)
['a', 2.0, 3]
>>> a_list.append(True)              (3)
>>> a_list
['a', 2.0, 3, True]
>>> a_list.extend(['four','Ω'])    (4)
>>> a_list
['a', 2.0, 3, True, 'four', 'Ω']
>>> a_list.insert(0,'Ω')        (5)
>>> a_list
['Ω', 'a', 2.0, 3, True, 'four', 'Ω']

(1) L’opérateur + concatène les listes afin de créer une nouvelle liste. Une liste peut contenir un nombre quelconque d’éléments. Il n’y a pas de limite de taille (à part la mémoire disponible). Toutefois, si la taille de la mémoire est cruciale, ayez à l’esprit qu’une concaténation de listes crée une nouvelle liste en mémoire. Dans ce cas, cette nouvelle liste est immédiatement affectée à la variable existante a_list. Le code de concaténation correspond donc en fait à un processus en deux étapes – concaténation puis affectation – ce qui peut consommer (temporairement) pas mal de mémoire si vous travaillez avec des listes de grande taille.

(2) Une liste peut contenir des éléments de n’importe quel type, et les éléments dans une liste unique n’ont pas besoin d’être tous du même type. Nous avons ici une liste qui contient une chaîne, un nombre à virgule flottante et un entier.

(3) La méthode append() ajoute un élément unique à la fin de la liste. Nous avons maintenant quatre types différents dans la liste.

(4) Les listes sont implémentées en tant que classes. « Créer » une liste consiste en fait à instancier une classe. En tant que classe, une liste possède des méthodes qui permettent d’y effectuer des modifications. La méthode extend() prend un seul argument, ici une liste, et ajoute chaque élément de celle-ci à la fin de la liste originale.

(5) La méthode insert() insère un élément unique dans une liste. Le premier argument est l’index du premier élément qui sera éjecté de sa position. Les éléments d’une liste ne doivent pas forcément être uniques, nous avons maintenant deux éléments distincts ayant la valeur 'Ω' : le premier élément, a_list[0], et le deuxième élément, a_list[6].

a_list.insert(0,value) est équivalent à la fonction unshift() en Perl. Elle insère un élément en tête de liste et tous les éléments suivants sont décalés d’une position pour faire de la place.

Voyons de plus près la différence entre append() et extend().

>>> a_list=['a','b','c']
>>> a_list.extend(['d','e','f'])        (1)
>>> a_list
['a', 'b', 'c', 'd', 'e', 'f']
>>> len(a_list)                    (2)
6
>>> a_list[-1]
'f'
>>> a_list.append(['g','h','i'])        (3)
>>> a_list
['a', 'b', 'c', 'd', 'e', 'f', ['g', 'h', 'i']]
>>> len(a_list)                    (4)
7
>>> a_list[-1]
['g', 'h', 'i']

(1) La méthode extend() prend un seul argument, qui est toujours une liste, et ajoute chaque élément de cette liste à a_list.

(2) Si vous commencez avec une liste de trois éléments, et si vous l’étendez avec une liste de trois éléments supplémentaires, vous obtenez finalement une liste de six éléments.

(3) Par ailleurs, la méthode append() prend un seul argument qui peut être de n’importe quel type. Vous appelez ici la méthode append() avec une liste de trois éléments.

(4) Si vous partez d’une liste de six éléments et que vous y ajoutez une liste de trois éléments, vous vous retrouvez avec une liste …de sept éléments. Pourquoi sept ? Parce que le dernier élément (que vous venez d’ajouter) est lui-même une liste. Les listes peuvent contenir n’importe quels types de données, y compris d’autres listes. C’est peut-être ce que vous vouliez obtenir, ou non. Mais c’est ce que vous avez demandé, et vous l’avez obtenu.

III-D-4 - Rechercher des valeurs dans une liste

>>> a_list=['a','b','new','mpilgrim','new']
>>> a_list.count('new')                      (1)
2
>>> 'new' in a_list                    (2)
True
>>> 'c' in a_list
False
>>> a_list.index('mpilgrim')                (3)
3
>>> a_list.index('new')                      (4)
2
>>> a_list.index('c')                    (5)
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
a_list.index('c')
ValueError: 'c' is not in list

(1) Comme vous vous en doutez, la méthode count() renvoie le nombre d’occurrences dans la liste d’une valeur donnée.

(2) Si tout ce que vous voulez savoir est si une valeur est présente ou non dans la liste, l’opérateur in est légèrement plus rapide que la méthode count(). L’opérateur in retourne toujours True ou False, il ne vous dira pas combien de fois la valeur apparaît dans la liste.

(3) Ni l’opérateur in, ni la méthode count(), ne vous diront où se trouve la valeur recherchée. Si vous avez besoin de savoir où se trouve la valeur dans la liste, faites appel à la méthode index(). Par défaut elle parcourra toute la liste, mais vous pouvez spécifier un second argument précisant l’endroit de la liste (débutant à 0) où commencer la recherche, et même un troisième argument précisant où arrêter.

(4) La méthode index() retournera la position de la première occurrence de l’argument dans la liste. Dans notre cas, 'new' apparaît deux fois dans la liste, en a_list[2] et en a_list[4], mais la méthode index() ne retournera que l’index de la première occurrence.

(5) Comme vous ne vous en doutez peut-être pas, si elle ne trouve pas la valeur dans la liste, la méthode index() lèvera une exception.

Attendez là, qu’est-ce que vous dites ? La méthode index() lève une exception si elle ne trouve pas la valeur dans la liste. Voilà qui diffère notablement des autres langages, qui retourneront un index invalide (comme -1). Cela peut paraître gênant au premier abord, mais je pense que vous finirez par l’apprécier. Cela signifie que votre programme plantera à la source du problème au lieu d’échouer de façon étrange et silencieuse un peu plus loin. Souvenez-vous que -1 est un index valide. Si la méthode index() retournait -1, cela pourrait mener à des séances de débogage pas très plaisantes.

III-D-5 - Retirer des éléments d’une liste

Les listes peuvent s’agrandir et se contracter automatiquement. Nous avons vu la partie extension. Il y a également différentes façons de retirer des éléments d’une liste.

>>> a_list=['a','b','new','mpilgrim','new']
>>> a_list[1]
'b'
>>> del a_list[1]            (1)
>>> a_list
['a', 'new', 'mpilgrim', 'new']
>>> a_list[1]            (2)
'new'

(1) Vous pouvez utiliser l’instruction del pour effacer un élément de la liste.

(2) Accéder à l’index 1 après l’avoir effacé ne se conclut pas par une erreur. Tous les éléments situés après l’élément effacé changent d’index positionnel de manière à « boucher les trous ».

Vous ne connaissez pas l’index de position ? Pas de problème, vous pouvez aussi retirer les éléments par valeur.

>>> a_list.remove('new')    (1)
>>> a_list
['a', 'mpilgrim', 'new']
>>> a_list.remove('new')    (2)
>>> a_list
['a', 'mpilgrim']
>>> a_list.remove('new')
Traceback (most recent call last):
File "<pyshell#17>", line 1, in <module>
a_list.remove('new')
ValueError: list.remove(x): x not in list

(1) Vous pouvez aussi retirer un élément de la liste avec la méthode remove(). La méthode remove() prend une valeur en paramètre et retire sa première occurrence de la liste. Encore une fois, les éléments suivants verront leur index décrémenté d’une unité afin de boucher les trous. Les listes ne contiennent jamais de trous.

(2) Vous pouvez appeler la méthode remove() aussi souvent que vous le voulez, mais elle lèvera une exception si elle ne trouve pas la valeur dans la liste.

III-D-6 - Retirer des éléments d’une liste : un tour en prime

Une autre méthode intéressante des listes est la méthode pop(). La méthode pop() est encore une autre manière de retirer des éléments d’une liste, mais avec un petit truc en plus.

>>> a_list=['a','b','new','mpilgrim']
>>> a_list.pop()        (1)
'mpilgrim'
>>> a_list
['a', 'b', 'new']
>>> a_list.pop(1)        (2)
'b'
>>> a_list
['a', 'new']
>>> a_list.pop()
'new'
>>> a_list.pop()
'a'
>>> a_list.pop()        (3)
Traceback (most recent call last):
File "<pyshell#28>", line 1, in <module>
a_list.pop()
IndexError: pop from empty list

(1) Quand on l’appelle sans argument, la méthode pop() retire le dernier élément de la liste et en retourne la valeur.

(2) Vous pouvez éjecter un article quelconque de la liste. Il suffit de passer en argument l’index de position de l’élément à retirer. La méthode pop() retire l’élément désigné, puis déplace les éléments suivants afin de boucher les trous, et enfin retourne la valeur de l’élément retiré.

(3) Faire appel à pop() sur une liste vide lève une exception.

Faire appel à la méthode pop() d’une liste sans fournir d’argument est équivalent à la fonction pop() de Perl. Elle retire le dernier élément de la liste et en retourne la valeur. Perl possède une autre fonction, shift(), qui retire le premier élément d’une liste et en retourne la valeur ; en Python cela équivaut à a_list.pop(0).

III-D-7 - Les listes dans un contexte booléen

Vous pouvez aussi utiliser la liste dans un contexte booléen, tel qu’une instruction if.

>>> def est_ce_vrai(quelque_chose):
if quelque_chose:
print("oui, c'est vrai")
else:
print("non, c'est faux")

>>> est_ce_vrai([])        (1)
non, c'est faux
>>> est_ce_vrai(['a'])        (2)
oui, c'est vrai
>>> est_ce_vrai([False])    (3)
oui, c'est vrai

(1) Dans un contexte booléen, une liste vide est fausse.

(2) Toute liste avec au moins un élément est vraie.

(3) Toute liste avec au moins un élément est vraie. La valeur de l’élément est sans importance.

III-E - Les Tuples

Un tuple est une liste immuable. Un tuple ne peut être modifié de quelque façon que ce soit une fois qu’il a été créé.

1. >>> a_tuple=('a','b','mpilgrim','z','exemple')    (1)

2. >>> a_tuple

3. ('a', 'b', 'mpilgrim', 'z', 'exemple')

4. >>> a_tuple[0]                        (2)

5. 'a'

6. >>> a_tuple[-1]                        (3)

7. 'exemple'

8. >>> a_tuple[1:3]                        (4)

9. ('b', 'mpilgrim')

(1) Un tuple est défini de la même façon qu’une liste, sauf que l’ensemble des éléments est compris entre parenthèses au lieu de crochets.

(2) Les éléments d’un tuple sont dans un ordre défini, exactement comme dans une liste. Les index des tuples ont pour base 0, exactement comme les listes, ainsi le premier élément d’un tuple non vide sera toujours a_tuple[0].

(3) Les index négatifs comptent à partir de la fin du tuple, exactement comme dans une liste.

(4) Le découpage (slicing) fonctionne également, exactement comme pour une liste. Quand vous découpez une liste, vous obtenez une nouvelle liste ; quand vous découpez un tuple, vous obtenez un nouveau tuple.

La différence principale entre les tuples et les listes, c’est que les tuples ne peuvent pas être modifiés. En termes techniques, les tuples sont immuables. En termes pratiques, ils ne possèdent pas de méthodes vous permettant de les modifier. Les listes ont des méthodes comme append(), extend(), insert(), remove() et pop(). Les tuples ne possèdent aucune de ces méthodes. Vous pouvez découper un tuple (parce que ça crée un nouveau tuple), et vous pouvez vérifier qu’un tuple contient une certaine valeur (parce que ça ne modifie pas le tuple), et…c’est tout.

1. >>> # suite de l'exemple précédent

2. >>> a_tuple

3. ('a', 'b', 'mpilgrim', 'z', 'exemple')

5. >>> a_tuple.append('new')        (1)

7. Traceback (most recent call last):

8. File "<pyshell#7>", line 1, in <module>

9. a_tuple.append('new')

10. AttributeError: 'tuple' object has no attribute 'append'

12. >>> a_tuple.remove('z')            (2)

14. Traceback (most recent call last):

15. File "<pyshell#8>", line 1, in <module>

16. a_tuple.remove('z')

17. AttributeError: 'tuple' object has no attribute 'remove'

19. >>> a_tuple.index('exemple')            (3)

20. 4

21. >>> 'z' in a_tuple                (4)

22. True

(1) Vous ne pouvez pas ajouter des éléments à un tuple. Un tuple n’a pas de méthode append() ou extend().

(2) Vous ne pouvez pas supprimer d’éléments dans un tuple, Les tuples n’ont pas de méthode remove() ou pop().

(3) Vous pouvez rechercher un élément dans un tuple, car ça ne modifie pas le tuple.

(4) Vous pouvez aussi utiliser l’opérateur in pour vérifier qu’un élément est présent dans le tuple.

Alors à quoi peuvent bien servir les tuples ?

- Les tuples sont plus rapides que les listes. Quand vous définissez un ensemble constant de valeurs et que tout ce à quoi vous allez vous en servir est de le consulter, utilisez un tuple.

- Cela rend votre code plus sûr de « protéger en écriture » des données qui ne sont pas appelées à être modifiées. Utiliser un tuple au lieu d’une liste, c’est comme disposer d’une instruction assert implicite qui montre que ces données sont des constantes, et qu’un effort de réflexion supplémentaire (et une fonction spécifique) est nécessaire pour contourner cela.

- Certains tuples peuvent être utilisés comme clés dans un dictionnaire (en particulier des tuples qui contiennent des valeurs immuables, comme des chaînes, des nombres et d’autres tuples). Les listes ne doivent jamais être utilisées comme des clés de dictionnaire, parce que les listes ne sont pas immuables.

- Les tuples peuvent être convertis en listes, et inversement. La fonction intégrée tuple() prend une liste en argument et en fait un tuple contenant les mêmes éléments, et la fonction list() prend un tuple en argument et en fait une liste. En fait, tuple() gèle la liste, et list() dégèle le tuple.

III-E-1 - Les tuples dans un contexte booléen

Vous pouvez utiliser les tuples dans un contexte booléen tel qu’une instruction if.

1. >>> def est_ce_vrai(quelque_chose):

2. if quelque_chose:

3. print("oui, c'est vrai")

4. else:

5. print("non, c'est faux")

8. >>> est_ce_vrai(())            (1)

9. non, c'est faux

10. >>> est_ce_vrai(('a','b'))        (2)

11. oui, c'est vrai

12. >>> est_ce_vrai((False,))        (3)

13. oui, c'est vrai

14. >>> type((False))                (4)

15. <class 'bool'>

16. >>> type((False,))

17. <class 'tuple'>

(1) Dans un contexte booléen, un tuple vide est faux.

(2) Tout tuple contenant au moins un élément est vrai.

(3) Tout tuple contenant au moins un élément est vrai. La valeur des éléments est sans importance. Mais que fait là cette virgule ?

(4) Pour créer un tuple avec un seul élément, il faut ajouter une virgule après cet élément. Sans cette virgule, Python estime que vous avez ajouté une paire de parenthèses supplémentaire inutile, mais ça ne crée pas un tuple.

III-E-2 - Affecter de multiples valeurs à la fois

Voici un raccourci de programmation vraiment super : en Python vous pouvez utiliser un tuple pour affecter de multiples valeurs d’un coup.

1. >>> v=('a',2,True)

2. >>> (x,y,z) = v        (1)

3. >>> x

4. 'a'

5. >>> y

6. 2

7. >>> z

8. True

(1) v est un tuple de trois valeurs et (x,y,z) est un tuple de trois variables. Affecter l’un à l’autre revient à affecter à chaque variable du premier la valeur correspondante du second.

Cela sert à toutes sortes de choses. Supposons que vous vouliez affecter des noms à une série de valeurs. Vous pouvez utiliser la fonction interne range() avec affectation multiple de variables pour affecter rapidement des valeurs consécutives.

1. >>> (LUNDI,MARDI,MERCREDI,JEUDI,VENDREDI,SAMEDI,DIMANCHE)=range(7)    (1)

2. >>> LUNDI                                            (2)

3. 0

4. >>> MARDI

5. 1

6. >>> DIMANCHE

7. 6

(1) La fonction interne range() construit une suite de nombres entiers. Techniquement parlant, la fonction range() retourne un itérateur, et non une liste ou un tuple, mais vous en apprendrez plus sur cette distinction plus tard. LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI et DIMANCHE sont les variables que vous définissez. (Cet exemple provient du module calendar, un petit module amusant qui imprime des calendriers, comme le programme UNIX cal. Le module calendar définit des constantes entières pour les jours de la semaine.)

(2) Maintenant chaque variable contient sa valeur : LUNDI vaut 0, MARDI vaut 1, etc.

Vous pouvez aussi utiliser l’affectation multiple de variables pour construire des fonctions qui retournent des valeurs multiples, simplement en retournant un tuple contenant toutes les valeurs. Le programme appelant peut traiter le résultat comme un unique tuple ou affecter les valeurs à une série de variables. Beaucoup de bibliothèques standard de Python font cela, y compris le module os dont vous ferez la connaissance dans le prochain chapitre.

III-F - Les ensembles (ou Sets)

Un ensemble, ou set, est un fourre-tout non ordonné de valeurs uniques. Un même ensemble peut contenir des valeurs de n’importe quel type immuable de données. Dès que vous avez deux ensembles, vous pouvez effectuer des opérations standard comme l’union, l’intersection ou la différence d’ensembles.

III-F-1 - Créer un ensemble

Commençons par le commencement. Créer un ensemble est facile.

1. >>> a_set={1}    (1)

2. >>> a_set

3. {1}

4. >>> type(a_set)    (2)

5. <class 'set'>

6. >>> a_set={1,2}

7. >>> a_set        (3)

8. {1, 2}

(1) Pour créer un ensemble (appelé set en Python) contenant une seule valeur, mettez cette valeur entre accolades.

(2) Les ensembles sont en fait implémentés en tant que classes, mais ne vous en souciez pas pour le moment.

(3) Pour créer un ensemble de plusieurs valeurs, séparez-les par des virgules et englobez le tout dans des accolades.

Vous pouvez aussi créer un ensemble à partir d’une liste.

1. >>> a_list=['a','b','mpilgrim',True,False,42]

2. >>> a_set=set(a_list)            (1)

3. >>> a_set                    (2)

4. {False, True, 'a', 42, 'b', 'mpilgrim'}

5. >>> a_list                    (3)

6. ['a', 'b', 'mpilgrim', True, False, 42]

(1) Pour créer un ensemble à partir d’une liste, utilisez la fonction set(). (Les conformistes qui savent comment les ensembles sont implémentés diront qu’il ne s’agit pas ici de l’appel d’une fonction, mais plutôt de l’instanciation d’une classe. Je promets que vous apprendrez la différence plus loin dans ce livre. Pour le moment, contentez-vous de retenir que set() agit comme une fonction et retourne un ensemble.)

(2) Comme je l’ai signalé plus haut, un ensemble peut contenir des valeurs de n’importe quel type. Et comme je l’ai signalé plus haut encore, les ensembles ne sont pas ordonnés. Cet ensemble ne se souvient pas de l’ordre initial des éléments de la liste qui a servi à le créer. Si vous ajoutiez des éléments à cet ensemble, il ne se souviendrait pas de l’ordre dans lequel vous les avez ajoutés.

(3) La liste originale reste inchangée.

Vous n’avez encore aucune valeur à ajouter ? Pas de problème : vous pouvez créer un ensemble vide.

1. >>> a_set=set()        (1)

2. >>> a_set            (2)

3. set()

4. >>> type(a_set)        (3)

5. <class 'set'>

6. >>> len(a_set)        (4)

7. 0

8. >>> not_sure={}        (5)

9. >>> type(not_sure)

10. <class 'dict'>

(1) Pour créer un ensemble vide, appelez la fonction set() sans argument.

(2) La représentation affichée d’un ensemble vide a l’air un peu bizarre. Vous attendiez-vous à {} peut-être ? Cela désignerait un dictionnaire vide, non un ensemble vide. Vous en apprendrez plus sur les dictionnaires un peu plus loin dans ce chapitre.

(3) Malgré la représentation bizarre, ceci est bien un ensemble…

(4) …et cet ensemble n’a pas d’éléments.

(5) À cause de bizarreries historiques héritées de Python 2, vous ne pouvez pas créer un ensemble vide avec deux accolades. Cela crée un dictionnaire vide, et non un ensemble vide.

III-F-2 - Modifier un ensemble

Il y a deux manières différentes d’ajouter un élément à un ensemble, la méthode add() et la méthode update().

1. >>> a_set={1,2}

2. >>> a_set.add(4)        (1)

3. >>> a_set

4. {1, 2, 4}

5. >>> len(a_set)        (2)

6. 3

7. >>> a_set.add(1)        (3)

8. >>> a_set

9. {1, 2, 4}

10. >>> len(a_set)        (4)

11. 3

(1) La méthode add() prend un unique argument, qui peut être de n’importe quel type, et ajoute la valeur à l’ensemble.

(2) Cet ensemble contient maintenant trois éléments.

(3) Les ensembles sont des sacs contenant des valeurs uniques. Si vous essayez d’ajouter une valeur qui existe déjà dans l’ensemble, rien ne se passe. Cela ne lèvera pas d’exception, c’est juste une non-opération.

1. L’ensemble contient toujours trois éléments.

1. >>> a_set={1,2,3}

2. >>> a_set

3. {1, 2, 3}

4. >>> a_set.update({2,4,6})            (1)

5. >>> a_set                        (2)

6. {1, 2, 3, 4, 6}

7. >>> a_set.update({3,6,9},{1,2,3,5,8,13})    (3)

8. >>> a_set

9. {1, 2, 3, 4, 5, 6, 8, 9, 13}

10. >>> a_set.update([10,20,30])            (4)

11. >>> a_set

12. {1, 2, 3, 4, 5, 6, 8, 9, 10, 13, 20, 30}

(1) La méthode update() prend comme argument un ensemble et ajoute tous ses éléments à l’ensemble initial. C’est comme si vous appeliez la méthode add() pour chacun des éléments.

(2) Les doublons sont ignorés, parce qu’un ensemble ne peut pas contenir de doublon.

(3) En réalité, vous pouvez appeler la méthode update() avec autant d’arguments que vous voulez. Si vous l’appelez avec deux ensembles, elle ajoutera tous les éléments des deux ensembles à l’ensemble original (en ignorant les doublons).

(4) La méthode update() accepte nombre de types de données différents, y compris les listes. Quand vous passez une liste en paramètre, la méthode update() ajoute tous les éléments de la liste à l’ensemble original (en ignorant les doublons, bien sûr).

III-F-3 - Retirer des éléments d’un ensemble

Il y a trois manières de retirer un élément d’un ensemble. Les deux premières, discard() et remove() ont une subtile différence.

1. >>> a_set={1,3,6,10,15,21,28,36,45}

2. >>> a_set

3. {1, 3, 36, 6, 10, 45, 15, 21, 28}

4. >>> a_set.discard(10)        (1)

5. >>> a_set

6. {1, 3, 36, 6, 45, 15, 21, 28}

7. >>> a_set.discard(10)        (2)

8. >>> a_set

9. {1, 3, 36, 6, 45, 15, 21, 28}

10. >>> a_set.remove(21)        (3)

11. >>> a_set

12. {1, 3, 36, 6, 45, 15, 28}

13. >>> a_set.remove(21)        (4)

14. Traceback (most recent call last):

15. File "<pyshell#24>", line 1, in <module>

16. a_set.remove(21)

17. KeyError: 21

(1) La méthode discard() prend un élément unique en argument et retire cette valeur de l’ensemble.

(2) Si vous appelez la méthode discard() avec une valeur qui n’existe pas, il ne se passe rien. C’est une non-opération.

(3) La méthode remove() prend également un élément unique en argument et retire également cette valeur de l’ensemble.

(4) Et voilà la différence : si la valeur n’existe pas dans l’ensemble, la méthode remove() lève une exception KeyError.

Comme les listes, les ensembles possèdent une méthode pop().

1. >>> a_set={1,3,6,10,15,21,28,36,45}

2. >>> a_set.pop()            (1)

3. 1

4. >>> a_set.pop()

5. 3

6. >>> a_set.pop()

7. 36

8. >>> a_set

9. {6, 10, 45, 15, 21, 28}

10. >>> a_set.clear()            (2)

11. >>> a_set

12. set()

13. >>> a_set.pop()            (3)

14. Traceback (most recent call last):

15. File "<pyshell#32>", line 1, in <module>

16. a_set.pop()

17. KeyError: 'pop from an empty set'

(1) La méthode pop() retire une seule valeur de l’ensemble et retourne cette valeur. Cependant, comme les ensembles sont non-ordonnés, il n’y a pas de « dernière » valeur dans un ensemble, aussi est-il impossible de contrôler quelle valeur sera retirée. C’est totalement aléatoire.

(2) La méthode clear() supprime tous les éléments de l’ensemble et laisse un ensemble vide. C’est équivalent à a_set = set() qui créerait un ensemble vide et écraserait la variable a_set avec cet ensemble vide.

(3) Tenter de supprimer une valeur dans un ensemble vide avec la méthode pop() provoque une exception KeyError.

III-F-4 - Opérations courantes sur les ensembles

Le type set de Python supporte plusieurs opérations courantes sur les ensembles.

1. >>> a_set={2,4,5,9,12,21,30,51,76,127,195}

2. >>> 30 in a_set                        (1)

3. True

4. >>> 31 in a_set

5. False

6. >>> b_set={1,2,3,5,6,8,9,12,15,17,18,21}

7. >>> a_set.union(b_set)                    (2)

8. {1, 2, 195, 4, 5, 3, 6, 8, 9, 76, 12, 15, 17, 18, 21, 30, 51, 127}

9. >>> a_set.intersection(b_set)                  (3)

10. {9, 2, 21, 12, 5}

11. >>> a_set.difference(b_set)                (4)

12. {195, 4, 76, 51, 30, 127}

13. >>> a_set.symmetric_difference(b_set)        (5)

14. {1, 3, 195, 6, 4, 8, 76, 15, 17, 18, 51, 30, 127}

(1) Pour tester si une valeur appartient à un ensemble, utilisez l’opérateur in. Ça fonctionne comme pour les listes.

(2) La méthode union() retourne un nouvel ensemble contenant tous les éléments figurant dans l’un OU l’autre ensemble.

(3) La méthode insertection() retourne un nouvel ensemble contenant tous les éléments figurant dans l’un ET l’autre ensemble.

(4) La méthode difference() retourne un nouvel ensemble contenant tous les éléments qui sont dans a_set mais pas dans b_set.

(5) La méthode symmetric_difference() retourne un nouvel ensemble contenant tous les éléments se trouvant exclusivement dans l’un des deux ensembles.

Trois de ces méthodes sont symétriques.

1. # suite de l'exemple précédent

2. >>> b_set.symmetric_difference(a_set)                        (1)

3. {1, 195, 4, 3, 6, 8, 76, 15, 17, 18, 51, 30, 127}

4. >>> b_set.symmetric_difference(a_set)==a_set.symmetric_difference(b_set)

5. True                                                (2)

6. >>> b_set.union(a_set)==a_set.union(b_set)                    (3)

7. True

8. >>> b_set.intersection(a_set)==a_set.intersection(b_set)            (4)

9. True

10. >>> b_set.difference(a_set)==a_set.difference(b_set)                (5)

11. False

(1) La différence symétrique entre a_set et b_set a l’air différente de la différence symétrique entre b_set et a_set, mais souvenez-vous, les ensembles sont non ordonnés. Deux ensembles contenant toutes les mêmes valeurs (sans aucune valeur restante) sont considérés comme égaux.

(2) Et c’est exactement ce qui se passe ici. Ne vous laissez pas induire en erreur par leur représentation écrite dans le shell Python. Ils contiennent les mêmes valeurs, donc ils sont égaux.

(3) L’union de deux ensembles est également symétrique.

(4) L’intersection de deux ensembles est également symétrique.

(5) La différence de deux ensembles n’est pas symétrique. Ça paraît logique, c’est comme soustraire un nombre d’un autre. L’ordre des opérandes compte.

Enfin, il y a quelques questions que vous pouvez vous poser sur les ensembles.

1. >>> a_set={1,2,3}

2. >>> b_set={1,2,3,4}

3. >>> a_set.issubset(b_set)    (1)

4. True

5. >>> b_set.issuperset(a_set)    (2)

6. True

7. >>> a_set.add(5)            (3)

8. >>> a_set.issubset(b_set)

9. False

10. >>> b_set.issuperset(a_set)

11. False

(1) a_set est un sous-ensemble de b_set. Tous les membres de a_set sont aussi membres de b_set.

(2) En posant la question à l’envers, b_set est un surensemble de a_set, tous les membres de a_set sont aussi membres de b_set.

(3) Dès que vous ajoutez une valeur dans a_set, qui n’est pas dans b_set, les deux tests retournent False.

III-F-5 - Les ensembles dans un contexte booléen

Vous pouvez utiliser les ensembles dans un contexte booléen comme une instruction if.

1. >>> def est_ce_vrai(quelque_chose):

2. if quelque_chose:

3. print("oui, c'est vrai")

4. else:

5. print("non, c'est faux")

8. >>> est_ce_vrai(set())        (1)

9. non, c'est faux

10. >>> est_ce_vrai({'a'})        (2)

11. oui, c'est vrai

12. >>> est_ce_vrai({False})    (3)

13. oui, c'est vrai

(1) Dans un contexte booléen, un ensemble vide est faux.

(2) N’importe quel ensemble avec au moins un élément est vrai.

(3) N’importe quel ensemble avec au moins un élément est vrai. La valeur de l’élément est sans importance.

III-G - Les dictionnaires (dictionaries)

Un dictionnaire est un ensemble non ordonné de paires clé-valeur. Quand vous ajoutez une clé à un dictionnaire, il vous faut aussi ajouter une valeur pour cette clé. (Vous pouvez toujours changer cette valeur plus tard). Les dictionnaires Python sont optimisés pour obtenir la valeur quand vous connaissez la clé, mais pas dans l’autre sens.

Un dictionnaire en Python, c’est comme un hash en Perl 5. En Perl 5, les variables contenant un hash commencent toujours par le caractère %. En Python, les variables peuvent être nommées n’importe comment, c’est Python qui en mémorise le type en interne.

III-G-1 - Créer un dictionnaire

Créer un dictionnaire est facile. La syntaxe est similaire à celle des ensembles, mais au lieu de valeurs, il contient des paires clé-valeur. Dans votre dictionnaire, vous pourrez accéder à une valeur par sa clé.

1. >>> a_dict={'server':'db.diveintopython3.org','database':'mysql'}    (1)

2. >>> a_dict

3. {'server': 'db.diveintopython3.org', 'database': 'mysql'}

4. >>> a_dict['server']                                    (2)

5. 'db.diveintopython3.org'

6. >>> a_dict['database']                                      (3)

7. 'mysql'

8. >>> a_dict['db.diveintopython3.org']                          (4)

9. Traceback (most recent call last):

10. File "<pyshell#7>", line 1, in <module>

11. a_dict['db.diveintopython3.org']

12. KeyError: 'db.diveintopython3.org'

(1) Pour commencer, vous créez un dictionnaire contenant deux éléments et vous l’affectez à la variable a_dict. Chaque élément est une paire clé-valeur et l’ensemble des éléments est compris entre accolades.

(2) 'server' est une clé, et sa valeur associée, référencée par a_dict['server'], est 'db.diveintopython3.org'.

(3) 'database' est une clé, et sa valeur associée, référencée par a_dict['database'], est 'mysql'.

(4) Vous pouvez accéder aux valeurs par leurs clés, mais vous ne pouvez pas accéder à une clé par sa valeur. Ainsi a_dict['server'] renvoie 'db.diveintopython3.org', mais a_dict['db.diveintopython3.org'] lève une exception, parce que 'db.diveintopython3.org' n’est pas une clé.

III-G-2 - Modifier un dictionnaire

Les dictionnaires n’ont pas de taille limite prédéfinie. Vous pouvez ajouter de nouvelles paires clé-valeur à tout moment, ou vous pouvez modifier la valeur d’une clé existante.

1. >>> # suite de l'exemple précédent

2. >>> a_dict

3. {'database': 'mysql', 'server': 'db.diveintopython3.org'}

4. >>> a_dict['database']='blog'                  (1)

5. >>> a_dict

6. {'database': 'blog', 'server': 'db.diveintopython3.org'}

7. >>> a_dict['user']='mark'                (2)

8. >>> a_dict                            (3)

9. {'database': 'blog', 'user': 'mark', 'server': 'db.diveintopython3.org'}

10. >>> a_dict['user']='dora'                (4)

11. >>> a_dict

12. {'database': 'blog', 'user': 'dora', 'server': 'db.diveintopython3.org'}

13. >>> a_dict['User']='mark'                (5)

14. >>> a_dict

15. {'database': 'blog', 'user': 'dora', 'User': 'mark', 'server': 'db.diveintopython3.org'}

(1) Vous ne pouvez pas avoir de clés dupliquées dans un dictionnaire. Affecter une valeur à une clé existante écrase l’ancienne valeur.

(2) Vous pouvez ajouter des paires clé-valeur à tout moment. La syntaxe est identique à celle de la modification d’une valeur existante.

(3) Le nouvel élément de dictionnaire (clé 'user', valeur 'mark') est situé au milieu. En fait, que les valeurs apparaissent ordonnées dans le premier exemple est une coïncidence. Qu’elles apparaissent ici dans le désordre est tout aussi fortuit.

(4) Affecter une nouvelle valeur à une clé existante du dictionnaire revient à remplacer l’ancienne valeur de la clé par la nouvelle.

(5) Ceci va-t-il remettre la valeur de la clé 'user' à l’ancienne valeur 'mark' ? Non ! Regardez la clé de plus près : le 'U' de 'User' est en majuscule. Les clés de dictionnaire sont sensibles à la casse, ainsi cette instruction crée-t-elle une nouvelle paire clé-valeur au lieu d’écraser la paire existante. Elles peuvent vous paraître similaires, mais aux yeux de Python, il s’agit de deux choses totalement différentes.

III-G-3 - Dictionnaires à valeurs mélangées

Les dictionnaires ne sont pas que pour les chaînes de caractères. Les valeurs d’un dictionnaire peuvent être de n’importe quel type, y compris les entiers, les booléens, des objets quelconques, ou même d’autres dictionnaires. Et dans un même dictionnaire, les valeurs n’ont pas à être du même type, vous pouvez le mélanger et les apparier à votre guise. Les clés de dictionnaires sont plus limitées, mais elles peuvent être des chaînes, des entiers et quelques autres types. Vous pouvez aussi mélanger et assortir les types des clés d’un dictionnaire.

En fait, vous avez déjà vu un dictionnaire avec des clés et des valeurs qui ne sont pas des chaînes de caractères, dans votre premier programme Python.

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

Décortiquons cela dans le shell interactif.

>>> SUFFIXES={1000:['KB','MB','GB','TB','PB','EB','ZB','YB'],1024:['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']}
>>> len(SUFFIXES)                    (1)
2
>>> 1000 in SUFFIXES                (2)
True
>>> SUFFIXES[1000]                (3)
['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
>>> SUFFIXES[1024]                (4)
['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
>>> SUFFIXES[1000][3]                (5)
'TB'

(1) Comme pour les listes et les ensembles, la fonction len() vous donne le nombre d’éléments du dictionnaire.

(2) Et comme pour les listes et les ensembles, vous pouvez utiliser l’opérateur in pour vérifier si une clé est définie dans le dictionnaire.

(3) 1000 est une clé du dictionnaire SUFFIXES. Sa valeur est une liste de huit éléments (huit chaînes de caractères pour être précis).

(4) De même, 1024 est une clé du dictionnaire SUFFIXES. Sa valeur est également une liste de huit éléments.

(5) Comme SUFFIXES[1000] est une liste, vous pouvez adresser les éléments individuels de la liste par leur index à base zéro.

III-G-4 - Dictionnaires dans un contexte booléen

Vous pouvez aussi utiliser un dictionnaire dans un contexte booléen, tel qu’une instruction if.

1. >>> def est_ce_vrai(quelque_chose):

2. if quelque_chose:

3. print("oui, c'est vrai")

4. else:

5. print("non, c'est faux")

8. >>> est_ce_vrai({})        (1)

9. non, c'est faux

10. >>> est_ce_vrai({'a':1})    (2)

11. oui, c'est vrai

(1) Dans un contexte booléen, un dictionnaire vide est faux.

(2) Tout dictionnaire contenant au moins une paire clé-valeur est vrai.

III-H - None

None est une constante spéciale en Python. C’est une valeur nulle. None n’est pas la même chose que False. None n’est pas 0. None n’est pas une chaîne vide. Comparer None à n’importe quoi d’autre que None retournera toujours False.

1. >>> type(None)

2. <class 'NoneType'>

3. >>> None==False

4. False

5. >>> None==0

6. False

7. >>> None==''

8. False

9. >>> None==None

10. True

11. >>> x=None

12. >>> x==None

13. True

14. >>> y=None

15. >>> x==y

16. True

III-H-1 - None dans un contexte booléen

Dans un contexte booléen, None est faux et not None est vrai.

1. >>> def est_ce_vrai(quelque_chose):

2. if quelque_chose:

3. print("oui, c'est vrai")

4. else:

5. print("non, c'est faux")

7. >>> est_ce_vrai(None)

8. non, c'est faux

9. >>> est_ce_vrai(not None)

10. oui, c'est vrai

III-I - Pour aller plus loin

Opérations booléennes (en anglais)

Types numériques (en anglais)

Types de séquences (en anglais)

Types d'ensembles ( en an gl ais)

Types de mappage (en anglais)

Module Fractions (en anglais)

Module math (en anglais)

PEP237 : unification des entiers et des entiers longs (en anglais)

PEP237 : modification de l'opérateur de division (en anglais)

IV - Chapitre 3 : Compréhensions de listes, de dictionnaires et de sets

« Notre imagination est mise à rude épreuve, non pas, comme dans la fiction, pour imaginer des choses qui n’existent pas vraiment, mais juste pour comprendre ce qui est là. »

Richard Feymann

IV-A - Plongée dans Python

Tout langage de programmation a cette fonctionnalité caractéristique, une chose complexe rendue simple intentionnellement. Si vous venez d'un autre langage, vous pouvez passer à côté, car votre précédent langage n'a pas rendu la même chose facile (celui-ci étant occupé à simplifier autre chose). Ce chapitre va vous enseigner les compréhensions de listes (NDT : aussi appelées listes en compréhensions ou listes en intentions), les compréhensions de dictionnaires, ainsi que les compréhensions de sets : trois concepts intimement centrés autour d'une seule puissante technique. Mais pour commencer, je voudrais faire un petit détour par deux modules qui vous aideront à naviguer dans votre système de fichier local.

IV-B - Travailler avec des fichiers et répertoires

Python 3 est fourni avec un module nommé os, pour « operating system » (système d'exploitation). Le module os contient une pléthore de fonctions afin de récupérer des informations relatives au système et dans certains cas, pour manipuler des répertoires locaux, fichiers, processus et variables d'environnement. Python fait de son mieux pour offrir une API unifiée supportant tous les systèmes d'exploitation afin que votre programme fonctionne sur n'importe quel ordinateur avec le moins de code spécifique à votre système que possible. Vous pouvez télécharger cette API ici :

IV-B-1 - Le répertoire courant

Lorsque vous débutez avec Python, vous allez passer beaucoup de temps avec le Shell Python. Tout au long de ce livre, vous verrez des exemples sous cette forme :

1. importez un des modules Python;

2. appelez une fonction de ce module ;

3. expliquez les résultats.

Si vous n'avez pas connaissance du répertoire courant, l'étape 1 échouera probablement avec un code d'erreur « ImportError ». Pourquoi ? Parce que Python va chercher le module exemple dans le dossier de recherche de modules , mais il ne le trouvera pas car le dossier `exemples` ne fait pas partie des répertoires du dossier de recherche. Pour surmonter ce problème, vous pouvez faire l'une des deux choses suivantes :

1. ajouter le répertoire « exemples » au dossier de recherche de modules ;

2. changer le répertoire courant pour celui du dossier « exemples ».

Le répertoire courant est une propriété invisible que Python garde en mémoire à tout instant. Il y a toujours un répertoire courant, que vous soyez dans le Shell Python, que vous fassiez tourner votre propre script Python en ligne de commande ou que vous fassiez tourner un script Python CGI (Common Gateway Interface) sur un serveur web quelque part.

Le module os dispose de deux fonctions pour travailler avec le répertoire courant.

1. python

2. >>> import os

3. >>> print(os.getcwd())

4. C:\Python31

5. >>> os.chdir('/Users/pilgrim/diveintopython3/exemples')

6. >>> print(os.getcwd())

7. C:\Users\pilgrim\diveintopython3\exemples

8. ```

- ligne 2 : le module os de la bibliothèque standard de Python ; vous pouvez l'importer à n'importe quel moment, n'importe où ;

- ligne 3 : utilise la fonction os.getcwd() afin d'obtenir le répertoire courant. Lorsque vous lancez le Shell Python, le répertoire courant débute par le répertoire où se situe l'exécutable du Shell. Sur Windows, cela dépend de l'emplacement où vous avez installé Python ; le répertoire par défaut est ici `C:\Python31`. Si vous lancez le Shell Python en ligne de commande, le répertoire courant débute par le répertoire où vous étiez lorsque vous aviez lancé `python3` ;

- ligne 5 : utilise la fonction os.chdir() pour modifier le répertoire courant. Lorsque j'ai appelé la fonction os.chdir(), j'ai utilisé un chemin de type Linux (des slashs (/) et non des backslashs (\) et pas de lettre pour le disque) même si je suis sur Windows. C'est l’un des cas où Python essaie de masquer les écarts entre les différents systèmes d'exploitation.

IV-B-2 - Travailler avec des noms de fichiers et de dossiers

Tant qu'à traiter du sujet des répertoires, je voudrais évoquer le module os.path. os.path contient des fonctions pour manipuler les noms de fichiers et de dossiers.

1. python

2. >>> import os

3. >>> print(os.path.join('/Users/pilgrim/diveintopython3/exemples/', 'humansize.py'))

4. /Users/pilgrim/diveintopython3/exemples/humansize.py

5. >>> print(os.path.join('/Users/pilgrim/diveintopython3/exemples', 'humansize.py'))

6. /Users/pilgrim/diveintopython3/exemples\humansize.py

7. >>> print(os.path.expanduser('~'))

8. c:\Users\pilgrim

9. >>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'exemples', 'humansize.py'))

10. c:\Users\pilgrim\diveintopython3\exeples\humansize.py

- ligne 3 : la fonction os.path.join() construit un nom de chemin à partir d'un ou plusieurs chemins partiels. Dans le cas présent, il concatène simplement des chaînes de caractères ;

- ligne 5 : dans ce cas légèrement moins trivial, appeler la fonction os.path.join() ajoutera un slash supplémentaire au nom du chemin avant d'ajouter le nom du fichier. C'est un backslash (`\`) au lieu d'un slash (`/`), car j'ai construit cet exemple sur Windows. Si vous reproduisez cet exemple sur Linux ou Max OS X, vous obtiendrez un slash à la place. Ne vous embêtez pas avec les slashes, préférez l'utilisation de os.path.join() et laissez Python faire ce qu'il faut pour vous ;

- ligne 7 : la fonction os.path.expanduser() va étendre un nom du répertoire qui utilise « ~ » pour représenter le répertoire par défaut de l'utilisateur courant (NDT : répertoire home). Cela fonctionne sur n'importe quelle plate-forme où l'utilisateur possède un répertoire par défaut, incluant Linux, Mac OS X et Windows. Le chemin retourné ne possède pas de slash à la fin, mais cela n'a aucune importance pour os.path.join() ;

- ligne 9 : en combinant ces techniques, vous pouvez aisément construire des noms de chemin pour dossiers et fichiers dans le répertoire par défaut de l'utilisateur. La fonction os.path.join() peut prendre un nombre indéfini d'arguments. J'étais plus que ravi lorsque j'ai découvert cela, addSlashIfNecessary() est l’une de ces petites fonctions stupides que j'ai toujours besoin d'écrire quand je développe ma boite à outils dans un nouveau langage. *N'écrivez pas* cette fonction stupide en Python ; des gens intelligents ont déjà pris soin de le faire pour vous.

os.path contient également des fonctions pour diviser un chemin entier, des noms de dossiers et de fichiers, en leur constituants élémentaires.

1. ```python

2. >>> pathname = '/Users/pilgrim/diveintopython3/exemples/humansize.py'

3. >>> os.path.split(pathname)

4. ('/Users/pilgrim/diveintopython3/exemples', 'humansize.py')

5. >>> (dirname, filename) = os.path.split(pathname)

6. >>> dirname

7. '/Users/pilgrim/diveintopython3/exemples'

8. >>> filename

9. 'humansize.py'

10. >>> (shortname, extension) = os.path.splitext(filename)

11. >>> shortname

12. 'humansize'

13. >>> extension

14. '.py'

- ligne 1 : la fonction split() divise un nom de chemin entier et retourne un tuple contenant le chemin et le nom de fichier ;

- vous vous rappelez quand je disais que vous pouviez assigner plusieurs variables à la fois à la sortie d'une fonction ? La fonction os.path.split() fait exactement cela. Vous assignez la valeur de retour de la fonction split() dans un tuple de deux variables. Chaque variable reçoit la valeur correspondant à l'élément du tuple renvoyé ;

- ligne 5 : la première variable, dirname, reçoit la valeur du premier élément du tuple retourné par os.path.split(), le chemin du fichier ;

- la seconde variable, filename, reçoit la valeur du second élément du tuple retourné par os.path.split(), le nom du fichier ;

- ligne 10 : os.path comprend la fonction os.path.splitext(), qui découpe un nom de fichier et retourne un tuple contenant le nom du fichier et son extension. Vous pouvez utiliser la même technique pour les assigner à des variables séparées.

IV-B-3 - Lister des répertoires

Le module glob est un autre outil de la bibliothèque standard de Python. C'est un moyen facile de récupérer le contenu d'un répertoire d'un point de vue programmation et il utilise le type de caractères de remplacement dont vous êtes peut-être déjà familier si vous travaillez en ligne de commande.

1. python

2. >>> os.chdir('/Users/pilgrim/diveintopython3/')

3. >>> import glob

4. >>> glob.glob('exemples/*.xml')

5. ['exemples\\feed-broken.xml',

6. 'exemples\\feed-ns0.xml',

7. 'exemples\\feed.xml']

8. >>> os.chdir('exemples/')

9. >>> glob.glob('*test*.py')

10. ['alphameticstest.py',

11. 'pluraltest1.py',

12. 'pluraltest2.py',

13. 'pluraltest3.py',

14. 'pluraltest4.py',

15. 'pluraltest5.py',

16. 'pluraltest6.py',

17. 'romantest1.py',

18. 'romantest10.py',

19. 'romantest2.py',

20. 'romantest3.py',

21. 'romantest4.py',

22. 'romantest5.py',

23. 'romantest6.py',

24. 'romantest7.py',

25. 'romantest8.py',

26. 'romantest9.py']

- ligne 4 : le module glob prend un caractère de remplacement et retourne le chemin de tous les fichiers et répertoires correspondants. Dans cet exemple, le caractère de remplacement est le chemin d'un répertoire plus « '*.xml », ce qui correspondra à tous les fichiers ayant l'extension « .xml » dans le sous-répertoire « exemples » ;

- ligne 8 : remplace maintenant le répertoire courant par le répertoire « exemples ». La fonction os.chdir() peut prendre des noms de chemins relatifs ;

- ligne 9 : vous pouvez inclure plusieurs caractères de remplacement pour votre motif de recherche (NDT: « search pattern » en anglais). Cet exemple trouve tous les fichiers dans le répertoire courant qui finissent par l'extension « .py » et qui contiennent le mot `test` n'importe où dans leur nom.

IV-B-4 - Récupérer les métadonnées d'un fichier

Tous les systèmes de fichiers modernes gardent en mémoire les métadonnées de chaque fichier : date de création, date de dernière modification, taille, etc. Python fourni une API unique pour accéder à ces données. Vous n'avez pas besoin d'ouvrir le fichier ; tout ce dont vous avez besoin est son nom.

1. ```python

2. >>> import os

3. >>> print(os.getcwd())

4. c:\Users\pilgrim\diveintopython3\exemples

5. >>> metadata = os.stat('feed.xml')

6. >>> metadata.st_mtime

7. 1247520344.9537716

8. >>> import time

9. >>> time.localtime(metadata.st_mtime)

10. time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17,

11. tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)

- ligne 3 : le répertoire courant est le dossier « exemples » ;

- ligne 5 : « `feed.xml » est un fichier du dossier « exemples ». Appeler la fonction os.stat() renvoie un objet qui contient plusieurs types différents de métadonnées à propos du fichier ;

- ligne 6 : st_mtime est la date de modification, mais dans un format qui n'est pas très utile. (Techniquement, c'est le nombre de secondes depuis « l'époque » (NDT: « Epoch time »), qui est définie comme la première seconde du 1er Janvier 1970. ) ;

- ligne 8 : le module time fait partie de la bibliothèque standard. Il contient des fonctions pour convertir différentes représentations temporelles, pour formater des valeurs temporelles en chaînes de caractères et gérer les fuseaux horaires ;

- ligne 9 : la fonction time.localtime() convertit la valeur temporelle des secondes depuis « l'époque » (à partir de l'attribut st_mtime retourné par la fonction os.stat()) dans une forme plus utile en années, mois, jours, heures, minutes, secondes, etc. Ce fichier a été modifié la dernière fois le 13 Juillet 2009, aux environs de 17h25 ;

1. ```python

2. # suite de l'exemple précédent

3. >>> metadata.st_size

4. 3070

5. >>> import humansize

6. >>> humansize.approximate_size(metadata.st_size)

7. '3.0 KiB'

8. ```

- ligne 3 : la fonction os.stat() retourne également la taille du fichier, grâce à l'attribut st_size. Le fichier « feed.xml » fait 3070 octets ;

- ligne 6: vous pouvez passer l'attribut st_size à la fonction approximate_size().

IV-B-5 - Construire un chemin absolu

Dans les précédentes sections, la fonction glob.glob() retournait une liste de chemins relatifs. Le premier exemple avait un chemin similaire à exemples\feed.xml et le second exemple avait un chemin relatif encore plus court comme romantest.py. Aussi longtemps que vous resterez dans le même répertoire courant, ces chemins relatifs fonctionneront pour ouvrir des fichiers et récupérer leurs métadonnées. Mais si vous souhaitez construire un chemin absolu, c'est-à-dire un chemin qui inclut tout le chemin du répertoire jusqu'à sa racine ou le nom du disque, alors vous aurez besoin de la fonction os.path.realpath().

1. python

2. >>> import os

3. >>> print(os.getcwd())

4. c:\Users\pilgrim\diveintopython3\exemples

5. >>> print(os.path.realpath('feed.xml'))

6. c:\Users\pilgrim\diveintopython3\exemples\feed.xml

IV-C - Compréhensions de listes (liste_intention)

Une compréhension de liste permet de créer une liste à partir d’une autre liste de façon compacte en appliquant une fonction à chaque élément de la liste initiale.

1. python

2. >>> a_list = [1, 9, 8, 4]

3. >>> [elem * 2 for elem in a_list]

4. [2, 18, 16, 8]

5. >>> a_list

6. [1, 9, 8, 4]

7. >>> a_list = [elem * 2 for elem in a_list]

8. >>> a_list

9. [2, 18, 16, 8]

- ligne 3 : afin de donner du sens à cette ligne, lisez-là de droite à gauche. a_list est la liste que vous créez. L’interpréteur Python parcours a_list un élément à la fois, assigne temporairement la valeur de chaque élément à la variable elem. Python applique ensuite la fonction elem * 2 et ajoute le résultat à la liste retournée ;

- ligne 5 : une compréhension de liste crée une nouvelle liste; elle ne change pas l'originale ;

- ligne 7 : assigner le résultat d'une compréhension de liste à une variable que vous êtes en train de mapper est sans danger. Python crée une nouvelle liste en mémoire et lorsque la création de la compréhension de liste est terminée, il assigne le résultat à la variable originale.

Vous pouvez utiliser n'importe quelle expression dans une compréhension de liste, y compris les fonctions du module os pour manipuler des fichiers et répertoires.

1. python

2. >>> import os, glob

3. >>> glob.glob('*.xml')

4. ['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']

5. >>> [os.path.realpath(f) for f in glob.glob('*.xml')]

6. ['c:\\Users\\pilgrim\\diveintopython3\\exemples\\feed-broken.xml',

7. 'c:\\Users\\pilgrim\\diveintopython3\\exemples\\feed-ns0.xml',

8. 'c:\\Users\\pilgrim\\diveintopython3\\exemples\\feed.xml']

- ligne 3 : ceci retourne une liste de tous les fichiers avec l'extension .xml du répertoire courant ;

- ligne 5 : cette compréhension de liste prend la liste de tous les fichiers avec l'extension .xml et la transforme en liste de chemins absolus.

Les compréhensions de listes peuvent également filtrer certains éléments, produisant un résultat qui peut être plus court que la liste originale.

1. python

2. >>> import os, glob

3. >>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000]

4. ['pluraltest6.py',

5. 'romantest10.py',

6. 'romantest6.py',

7. 'romantest7.py',

8. 'romantest8.py',

9. 'romantest9.py']

- ligne 3 : pour filtrer une liste, vous pouvez inclure une condition à la fin de la compréhension de liste. L'expression après le mot clef if va évaluer chaque élément de la liste. Si l'expression est évaluée comme vraie (True), l'élément sera inclus dans la sortie. Cette expression examine tous les fichiers .py du répertoire courant, et la condition if filtre cette liste en testant si la taille de chaque fichier est plus grande que 6000 octets. Il y a six fichiers satisfaisant ce critère, donc la compréhension de liste retourne une liste de six noms de fichiers.

Tous les exemples de compréhensions de listes présentés jusqu'à présent contenaient des expressions simples : multiplier un nombre par une constante, appeler une seule fonction ou simplement retourner les éléments de la liste originelle (après filtrage). Mais il n'y a pas de limite à la complexité que peut avoir une compréhension de liste.

1. python

2. >>> import os, glob

3. >>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')]            1

4. [(3074, 'c:\\Users\\pilgrim\\diveintopython3\\exemples\\feed-broken.xml'),

5. (3386, 'c:\\Users\\pilgrim\\diveintopython3\\exemples\\feed-ns0.xml'),

6. (3070, 'c:\\Users\\pilgrim\\diveintopython3\\exemples\\feed.xml')]

7. >>> import humansize

8. >>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')]  2

9. [('3.0 KiB', 'feed-broken.xml'),

10. ('3.3 KiB', 'feed-ns0.xml'),

11. ('3.0 KiB', 'feed.xml')]

- cette compréhension de liste trouve tous les fichiers .xml du répertoire courant, récupère la taille de chaque fichier (en appelant la fonction os.stat()) et construit un tuple avec la taille et le chemin absolu de chaque fichier (en appelant la fonction os.path.realpath()) ;

- cette compréhension de liste construite sur la base de la précédente appelle la [fonction approximate_size()] avec la taille de chaque fichier .xml.

IV-D - Compréhensions de dictionnaires

Une compréhension de dictionnaire est comme une compréhension de liste, mais elle construit un dictionnaire en lieu et place d'une liste.

1. python

2. >>> import os, glob

3. >>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')]

4. >>> metadata[0]

5. ('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,

6. st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,

7. st_mtime=1247520344, st_ctime=1247520344))

8. >>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')}

9. >>> type(metadata_dict)

10. <class 'dict'>

11. >>> list(metadata_dict.keys())

12. ['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',

13. 'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',

14. 'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',

15. 'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',

16. 'pluraltest4.py']

17. >>> metadata_dict['alphameticstest.py'].st_size

18. 2509

- ligne 3 : ceci n'est pas une compréhension de dictionnaire, c'est une compréhension de liste (liste_intention). Elle trouve tous les fichier avec l'extension .py avec « test » dans leur nom, puis construit un tuple composé du nom du fichier et de ses métadonnées (en appelant la fonction os.stat()) ;

- ligne 4 : chaque élément retourné de la liste est un tuple ;

- ligne 8 : ceci est une compréhension de dictionnaire. La syntaxe est similaire à celle d'une compréhension de liste, avec deux différences. Premièrement, elle est entourée d'accolades au lieu de crochets. Deuxièmement, à la place d'une seule expression pour chaque élément, elle contient deux expressions séparées par deux points (« : »). L'expression avant les deux points (« f » dans cet exemple) est la clef du dictionnaire ; l'expression après les deux points (os.stat(f) dans cet exemple) est la valeur ;

- ligne 9 : une compréhension de dictionnaire renvoie un dictionnaire ;

- ligne 11 : les clefs de ce dictionnaire sont simplement les noms des fichiers renvoyés par l'appel à glob.glob('*test*.py') ;

- ligne 17 : la valeur associée à chaque clef est la sortie de la fonction os.stat(). Cela signifie que nous pouvons chercher un fichier par son nom dans ce dictionnaire afin de récupérer ses métadonnées. Un des éléments des métadonnées est l'attribut st_size, la taille du fichier. Le fichier alphameticstest.py fait ainsi 2509 octets.

Comme pour les compréhensions de listes, vous pouvez définir une condition dans une compréhension de dictionnaire afin de filtrer les entrées de la séquence en se basant sur une expression évaluée pour chaque élément.

1. ```python

2. >>> import os, glob, humansize

3. >>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')}                                  1

4. >>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \

5. ...                   for f, meta in metadata_dict.items() if meta.st_size > 6000}          2

6. >>> list(humansize_dict.keys())                                                             3

7. ['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']

8. >>> humansize_dict['romantest9']                                                            4

9. '6.5 KiB'

10. ```

- cette compréhension de dictionnaire construit une liste de tous les fichiers du répertoire courant (glob.glob('*')), récupère les métadonnées de chaque fichier (os.stat(f)) et construit un dictionnaire dont les clefs sont les noms des fichiers et les valeurs les métadonnées ;

- cette compréhension de dictionnaire construite à partir de la précédente, filtre et ne retient pas les fichiers ayant une taille inférieure à 6000 octets (if meta.st_size > 6000). Elle utilise la liste filtrée pour construire un dictionnaire dont les clefs sont les noms de fichiers sans l'extension (os.path.splitext(f)[0]) et dont les valeurs sont la taille approchée des fichiers (humansize.approximate_size(meta.st_size)) ;

- comme vous avez pu le voir dans l'exemple précédent, il y a six fichiers d'une taille supérieure à 6000 octets, ainsi il y a six éléments dans ce dictionnaire ;

- la valeur associée à chaque clef est une chaîne de caractères renvoyée par la fonction approximate_size().

IV-D-1 - D'autres choses sympathiques à faire avec les compréhensions de dictionnaires

Voici une astuce avec les compréhensions de dictionnaires pouvant s'avérer utile : interchanger les clefs et valeurs d'un dictionnaire.

1. python

2. >>> a_dict = {'a': 1, 'b': 2, 'c': 3}

3. >>> {value:key for key, value in a_dict.items()}

4. {1: 'a', 2: 'b', 3: 'c'}

Bien entendu, cela ne fonctionne que si les valeurs du dictionnaire sont non mutables, comme les chaînes de caractères ou les tuples. Si vous souhaitez essayer ceci avec un dictionnaire qui contient des listes, cela échouera superbement :

1. python

2. >>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}

3. >>> {value:key for key, value in a_dict.items()}

4. Traceback (most recent call last):

5. File "<stdin>", line 1, in <module>

6. File "<stdin>", line 1, in <dictcomp>

7. TypeError: unhashable type: 'list'

IV-E - Compréhensions de sets

À ne pas oublier : les sets ont également leur propre syntaxe en compréhension. Elles sont remarquablement similaires à celle des compréhensions de dictionnaires. La seule différence est que les sets n'ont que des *valeurs* en lieu et place des paires clef/valeur des dictionnaires.

1. python

2. >>> a_set = set(range(10))

3. >>> a_set

4. {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

5. >>> {x ** 2 for x in a_set}

6. {0, 1, 4, 81, 64, 9, 16, 49, 25, 36}

7. >>> {x for x in a_set if x % 2 == 0}

8. {0, 8, 2, 4, 6}

9. >>> {2**x for x in range(10)}

10. {32, 1, 2, 4, 8, 64, 128, 256, 16, 512}

- ligne 5 : les compréhensions de sets peuvent prendre un set en entrée. Cette compréhension de set calcule le carré d'un set de nombres de 0 à 9 ;

- ligne 7 : tout comme les compréhensions de listes et les compréhensions de dictionnaires, les compréhensions de sets peuvent contenir une clause conditionnelle if afin de filtrer chaque élément avant de le retourner dans le set résultat ;

- ligne 9 : les compréhensions de sets n'ont pas besoin de prendre un set en entrée, elles peuvent prendre n'importe quelle séquence.

IV-F - Lectures pour aller plus loin

- le module os.

- os - Accès portable aux fonctionnalités spécifiques au système d'exploitation.

- le module os.path.

- le module glob.

- glob - Recherche de motifs des noms de fichiers.

- le module time.

- time - Fonctions pour manipuler le temps horloge.

- compréhensions de listes.

- compréhensions de listes imbriquées.

- techniques de boucles.

V - Chapitre 4 - Strings

« I'm telling you this 'cause you're one of my friends. My alphabet starts where your alphabet ends! » - Dr. Seuss, On Beyond Zebra!

V-A - Quelques petites choses ennuyeuses que vous devez comprendre avant de plonger

Peu de personnes y prêtent attention, mais le texte est incroyablement compliqué. Commencez avec l'alphabet. Les habitants de l’île de Bougainville ont le plus petit alphabet du monde ; leur alphabet, le Rotokas, est composé de seulement 12 lettres : A, E, G, I, K, O, P, R, S, T, U et V. À l'autre extrémité du spectre, les langues comme le chinois, le japonais et le coréen ont des milliers de caractères. L'anglais, bien sûr, a 26 lettres – 52, si vous comptez les majuscules et les minuscules séparément - plus une poignée de symboles de ponctuation : !@#$%&.

Quand vous parlez de « texte », vous pensez probablement aux « caractères et symboles sur l'écran de mon ordinateur ». Mais les ordinateurs ne travaillent pas avec des caractères et des symboles ; ils fonctionnent avec des bits et des octets. Chaque morceau de texte que vous avez vu sur un écran d'ordinateur est en fait stocké grâce à un encodage de caractères particulier. Dit grossièrement, l‘encodage de caractères fournit une correspondance entre ce que vous voyez à l'écran et ce que votre ordinateur stocke effectivement en mémoire et sur le disque. Il y a de nombreux systèmes d’encodage des caractères, certains optimisés pour une langue en particulier, comme le russe ou le chinois ou l'anglais, et d'autres qui peuvent être utilisés pour de multiples langages.

En réalité, c'est plus compliqué que cela. De nombreux caractères sont communs à plusieurs encodages, mais chaque encodage peut utiliser une séquence d'octets différente pour réellement stocker ces caractères en mémoire ou sur le disque. Ainsi, vous pouvez penser à l’encodage de caractères comme à une sorte de clef de déchiffrement. Dès que quelqu'un vous fournit une séquence d’octets - un fichier, une page web, n'importe quoi - et annonce que c'est du « texte », vous avez besoin de savoir quel encodage de caractères est utilisé pour que vous soyez en mesure de décoder ces octets en caractères. Si une mauvaise clef est fournie, ou aucune clef, vous vous retrouvez avec la tâche non enviable de craquer le code par vous-même. Il y a de fortes chances que vous n'y parveniez pas, et que le résultat soit incompréhensible.

Vous avez certainement déjà vu des pages web, avec d'étranges caractères ressemblant à des points d'interrogation là où des apostrophes devraient être. Cela signifie le plus souvent que l'auteur de la page n’a pas déclaré l'encodage de caractères correctement, que votre navigateur Internet a essayé de le deviner et que le résultat était un mixe de caractères attendus et non attendus. En anglais, c'est seulement gênant ; dans d'autres langues, le résultat peut être complètement illisible.

Il y a des encodages de caractères pour chaque langue majeure dans le monde. Comme chaque langue est différente et qu’historiquement, la mémoire et l'espace disque étaient coûteux, chaque encodage de caractères est optimisé pour une langue en particulier. J'entends par là que chaque encodage utilise les mêmes nombres (de 0 à 255) pour représenter tous les caractères de cette langue. Par exemple, vous êtes probablement familier avec l'encodage ASCII qui stocke les caractères de la langue anglaise sous la forme de nombres allant de 0 à 127. (65 étant le « A » majuscule, 97 étant le « a » minuscule, etc.). L'anglais a un alphabet très simple, il peut donc être entièrement exprimé avec moins de 128 nombres. Pour ceux d'entre vous qui savent compter en base 2, cela représente 7 des 8 bits d'un octet.

Les langues de l'Europe de l'Ouest comme le français, l'espagnol, et l'allemand ont plus de lettres que l'anglais. Ou, plus précisément, elles ont des lettres combinées avec différents signes diacritiques, comme le caractère ñ en espagnol. L’encodage de caractères le plus courant pour ces langues est le CP-1252, aussi appelé « Windows-1252 » car il est largement utilisé sur Microsoft Windows. L'encodage CP-1252 partage des caractères avec l'ASCII dans la plage 0-127, mais s'étend à la plage 128-255 pour les caractères comme le « n » avec un tilde au-dessus (241), le « u » avec deux points au-dessus (252), etc. Cependant, c’est toujours un encodage sur un octet ; le plus grand nombre possible, 255, tient toujours dans un octet.

Viennent ensuite les langues comme le chinois, le japonais, et le coréen, qui ont tellement de caractères qu'elles nécessitent un ensemble de caractères encodés sur plusieurs octets. Chaque « caractère » est représenté par un nombre codé sur deux octets, allant de 0 à 65535. Mais les différents encodages multioctets partagent toujours le même problème que les encodages à un seul octet, à savoir qu'ils utilisent chacun les mêmes nombres pour représenter différentes choses. La gamme de nombres est simplement plus large, car il y a beaucoup plus de caractères à représenter.

C'était acceptable dans un monde non connecté, où le « texte » était quelque chose que vous aviez vous-même tapé et parfois imprimé. Le « texte brut » (1) n'existait pas. Le code source était en ASCII, et tout le monde utilisait un logiciel de traitement de texte qui définissait son propre format (non-texte) et qui conservait l'information sur l’encodage de caractères ainsi que les styles employés, etc. Les personnes lisaient ces documents avec le même logiciel de traitement de texte que celui de l'auteur, ainsi tout fonctionnait plus ou moins correctement.

Pensez maintenant à la montée des réseaux mondiaux tels que la messagerie électronique et le Web. Une grande quantité de « textes bruts » s'échangeant autour de la planète, sont écrits sur un ordinateur, transmis par un deuxième ordinateur et reçus et affichés sur un troisième ordinateur. Les ordinateurs ne traitent que des chiffres, mais ces chiffres peuvent signifier différentes choses. Oh non ! Que faire ? Et bien, les systèmes devaient être conçus pour transporter l’information d'encodage avec chaque élément de «texte brut». Souvenez-vous, c'est la clef de déchiffrement qui fait correspondre les nombres lisibles par l'ordinateur en caractères lisibles par l'homme. Une clef de déchiffrement manquante signifie du texte tronqué, du charabia ou pire.

Maintenant, essayez d’imaginer vouloir stocker plusieurs morceaux de texte au même endroit, comme dans la même table de base de données contenant tous les courriels que vous avez reçus. Vous devez toujours stocker l'encodage de caractères à côté de chaque morceau de texte pour pouvoir l'afficher correctement. Vous pensez que c'est difficile ? Essayez de faire une recherche dans votre base de données de courriels, ce qui signifie que vous convertissez plusieurs encodages à la volée. Cela ne vous semble-t-il pas amusant ?

Pensez maintenant aux documents multilingues, où des caractères de plusieurs langues sont côte à côte dans le même document (indice : les programmes qui essayaient de faire cela utilisaient généralement des codes d'échappement pour changer de « mode ». Pouf, vous êtes en mode russe koi8-r, donc 241 signifie Я ; pouf, vous êtes maintenant en mode Mac Greek, donc 241 signifie ώ). Et bien sûr, vous voudrez aussi faire des recherches dans ces documents.

Maintenant, pleurez, parce que tout ce que vous pensiez savoir au sujet des chaînes de caractères est faux, et que le « texte brut» n'existe pas.

V-B - Unicode

Entrée Unicode

L'Unicode est un système conçu afin de représenter chaque caractère de chaque langue. Unicode représente chaque lettre, chaque caractère ou idéogramme, comme un nombre codé sur 4 octets. Chaque nombre représente un caractère unique utilisé dans au moins un mot de toutes les langues du monde (tous les nombres ne sont pas utilisés, mais plus de 65535 d'entre eux le sont, donc 2 octets ne suffiraient pas). Les caractères employés dans plusieurs langues ont généralement le même nombre, à moins qu'il y ait une bonne raison étymologique à ce que ce ne soit pas le cas. Quoi qu'il en soit, il y a exactement 1 nombre par caractère et exactement 1 caractère par nombre. Chaque nombre a une signification unique ; il n'y a pas de « modes » à suivre. u+0041 signifie toujours 'A', même dans une langue qui ne possède pas de 'A'.

À priori, cela semble être une bonne idée. Un encodage pour les diriger tous. Plusieurs langues dans un même document. Plus de « commutation de mode » pour basculer entre les encodages en cours de flux. Immédiatement, une question évidente devrait vous sauter aux yeux. Quatre octets ? Cela semble terriblement inutile, en particulier pour les langues telles que l'anglais ou l'espagnol, qui ont besoin de moins d'un octet (256 nombres) pour exprimer chaque caractère possible. En fait, c'est même du gaspillage pour les langues basées sur des idéogrammes (comme le chinois), dont les besoins n'excèdent jamais plus de deux octets par caractère.

Il existe un encodage Unicode qui utilise quatre octets par caractère. Il s'appelle UTF-32, car 32 bits = 4 octets. UTF-32 est un encodage simple ; il prend chaque caractère Unicode (un nombre de 4 octets) et représente ce caractère avec ce même nombre. Cela présente certains avantages, le plus important étant que vous pouvez trouver le Nième caractère d'une chaîne en un temps constant, car le Nième caractère commence à l'octet 4xN. Il présente également plusieurs inconvénients, le plus évident étant qu'il prend quatre octets pour chaque caractère.

Bien qu'il y ait beaucoup de caractères Unicode, il s'avère que la plupart des gens n'utiliseront jamais plus que les 65535 premiers. Il existe donc un autre codage Unicode appelé UTF-16, car 16 bits = 2 octets. UTF-16 encode chaque caractère de 0 à 65535 en 2 octets, puis fait appel à des astuces si vous avez réellement besoin de représenter les caractères Unicode rarement utilisés du « plan astral(2) », au-delà de 65535. L'avantage le plus évident : UTF-16 est deux fois plus efficace en termes de matière d'espace utilisé que UTF-32, car chaque caractère nécessite seulement deux octets au lieu des quatre requis par UTF-32 (à l'exception de ceux qui n'en ont pas besoin). Et vous pouvez toujours facilement retrouver le Nième caractère d'une chaîne en un temps constant, si vous présumez que la chaîne n'inclut aucun caractère du plan astral, ce qui est une bonne hypothèse tant que ce moment ne se présente pas.

Mais il existe également des inconvénients non évidents à la fois pour UTF-32 et UTF-16. Les octets sont stockés de différentes manières par les différents systèmes informatiques. Cela signifie que le caractère U+4E2D peut être stocké en UTF-16 comme soit 4E 2D, soit 2D 4E, selon que le système est big-endian(3) ou little-endian(4). (Pour UTF-32, il y a encore plus de possibilités pour ordonner les octets). Tant que vos documents ne quittent jamais votre ordinateur, vous êtes tranquille – les différents programmes d'un même ordinateur utiliseront tous le même ordre d'octets. Cependant, à la minute où vous souhaitez transférer des documents d'un système à un autre, peut-être sur Internet, vous aurez besoin d'un moyen pour indiquer dans quel ordre vos octets sont stockés. Autrement, le système recevant ces données n'a aucun moyen de savoir si la séquence de deux octets 4E 2D signifie U+4E2D ou U+2D4E.

Pour résoudre ce problème, les encodages Unicode multioctets définissent une « marque d'ordre d'octets » qui est un caractère spécial non imprimable que vous pouvez inclure au début de votre document pour indiquer l'ordre de vos octets. Pour UTF-16, la marque d'ordre d'octets est U+FEFF. Si vous recevez un document encodé en UTF-16 commençant par l'octet FF FE, vous savez que l'ordre est dans un sens ; s'il commence par FE FF, vous savez que l'ordre est inversé.

Cependant, UTF-16 n'est pas tout à fait idéal, surtout si vous utilisez beaucoup de caractères ASCII. Si vous y réfléchissez, même une page web en chinois contiendra de nombreux caractères ASCII : tous les éléments et attributs entourant les caractères chinois imprimables. Il est bien de pouvoir trouver le Nième caractère en un temps constant, mais il y toujours ce problème récurrent des caractères du plan astral, ce qui signifie que vous ne pouvez pas garantir que chaque caractère a exactement deux octets, vous ne pouvez donc pas réellement déterminer le Nième caractère en un temps constant, sauf si vous maintenez un index séparé. Et bon sang, il y a énormément de texte en ASCII dans le monde…

D'autres personnes ont réfléchi à ces questions et ont proposé une solution :

UTF-8

UTF-8 est un système d’encodage à longueur variable pour Unicode. Autrement dit, différents caractères utilisent un nombre d'octets différent. Pour les caractères ASCII (de A à Z, etc.), UTF-8 utilise un seul octet par caractère. En fait, il utilise exactement les mêmes octets ; en UTF-8, les 128 premiers caractères (de 0 à 127) sont impossibles à distinguer de l'ASCII. Les caractères « latin étendu » comme ñ et ö prennent deux octets. (Les octets ne sont pas simplement des points de code Unicode tels qu’ils le seraient en UTF-16 ; cela implique quelques sérieux bricolages). Les caractères chinois comme 中 prennent trois octets. Les caractères rarement utilisés du « plan astral » prennent quatre octets.

Inconvénients : comme chaque caractère peut prendre un nombre d'octets différent, trouver le Nième caractère est une opération O(N), c'est-à-dire que plus la chaîne de caractères est longue, plus la recherche d'un caractère spécifique prend du temps. En outre, encoder des caractères en octets et inversement implique un peu de bricolage.

Avantages : cet encodage est super efficace pour les caractères ASCII courants. Moins mauvais que UTF-16 pour les caractères latins étendus. Mieux que UTF-32 pour les caractères chinois. Aussi (et vous devrez me faire confiance à ce sujet, parce que je ne vais pas vous le démontrer par calcul), en raison de la nature du bricolage sur les octets, il n'y a pas de problème d'ordre des octets. Un document encodé en UTF-8 utilise exactement le même flux d'octets sur n'importe quel ordinateur.

V-C - Plongeons

En Python 3, toutes les chaînes sont des séquences de caractères Unicode. Il n'existe pas de chaîne Python encodée en UTF-8, ni de chaîne Python encodée en CP-1252. « Cette chaîne est-elle codée en UTF-8 ? » est une question invalide en Python 3. UTF-8 est une manière d’encoder des caractères en une séquence d'octets. Si vous souhaitez transformer une chaîne en séquence d'octets dans un encodage de caractères particulier, Python 3 peut vous aider. Si vous souhaitez convertir une séquence d'octets en chaîne, Python 3 peut également vous aider. Les octets ne sont pas des caractères ; les octets sont des octets. Les caractères sont une abstraction. Une chaîne est une séquence de ces abstractions.

1. >>> s = '深入 Python'     1

2. >>> len(s)               2

3. 9

4. >>> s[0]                 3

5. '深'

6. >>> s + ' 3'             4

7. '深入 Python 3'

1. Pour créer une chaîne, encadrez-la de guillemets. Les chaînes de caractères Python peuvent être définies soit entre des guillemets simples (') soit entre des guillemets doubles ("). La fonction interne len() renvoie la longueur d'une chaîne de caractères, c'est-à-dire le nombre de caractères. C'est la même fonction que celle que vous utilisez afin de déterminer la longueur d'une liste, d'un tuple, d'un set ou d'un dictionnaire. Une chaîne de caractères est comme un tuple de caractères. Tout comme la récupération d'un élément d'une liste, vous pouvez récupérer un caractère d'une chaîne en utilisant son index.Comme pour les listes, vous pouvez concaténer des chaînes de caractères en utilisant l'opérateur +.

V-D - Formater les chaînes de caractères

Regardons à nouveau le script humansize.py :

humansize.py

1. SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],         1

2. 1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

4. def approximate_size(size, a_kilobyte_is_1024_bytes=True):

5. '''Convertit la taille d'un fichier dans une forme lisible par humain.  2

7. Arguments:

8. size -- La taille du fichier en octets

9. a_kilobyte_is_1024_bytes -- Si vrai (True) (par défaut), utilise un multiple

10. de 1024. Si faux (False), utilise un multiple de

11. 1000.

13. Returns: string

14. '''                                                                     3

15. if size < 0:

16. raise ValueError('La taille doit être un nombre positif')           4

18. multiple = 1024 if a_kilobyte_is_1024_bytes else 1000

19. for suffix in SUFFIXES[multiple]:

20. size /= multiple

21. if size < multiple:

22. return '{0:.1f} {1}'.format(size, suffix)                       5

24. raise ValueError('Nombre trop grand')

1. ‘KB’, ‘MB’, ‘GB’… sont des chaînes de caractères ;

2. Les docstrings de la fonction sont des chaînes de caractères. Cette docstring(5) s'étend sur plusieurs lignes, donc elle utilise trois guillemets à la suite pour commencer et terminer la chaîne de caractères ;

3. Ces trois guillemets à la suite clôturent la docstring ;

4. Ceci est une autre chaîne de caractères qui est passée à l'erreur d’exception afin d'avoir un message d'erreur humainement lisible ;

5. C'est un… waouh, qu'est-ce que c'est que ça ?

Python 3 prend en charge le formatage des valeurs en chaînes. Bien que cela puisse inclure des expressions très compliquées, l'utilisation la plus élémentaire consiste à insérer une valeur dans une chaîne avec un seul espace réservé.

1. >>> username = 'mark'

2. >>> password = 'PapayaWhip'                             1

3. >>> "{0}'s password is {1}".format(username, password)  2

4. "mark's password is PapayaWhip"

1. Non, mon mot de passe n'est pas réellement PapayaWhip(6) ;

2. Beaucoup de choses se passent ici. Premièrement, ceci est un appel à une méthode sur une chaîne de caractères littérale. Les chaînes de caractères sont des objets, et les objets ont des méthodes. Deuxièmement, l'expression entière est évaluée comme une chaîne de caractères. Troisièmement, {0} et {1} sont des champs de remplacement qui sont remplacés par les arguments passés à la méthode format().

V-D-1 - Les champs de noms composés

L'exemple précédent montre le cas le plus simple où les champs de remplacement sont simplement des entiers. Les champs de remplacement d'entiers sont traités comme des index dans la liste d'arguments de la méthode format(). Cela signifie que {0} est remplacé par le premier argument (username dans ce cas), que {1} est remplacé par le deuxième argument (password), etc. Vous pouvez avoir autant d’index de position que d'arguments et autant d'arguments que vous le souhaitez. Mais les champs de remplacement sont beaucoup plus puissants que cela.

1. >>> import humansize

2. >>> si_suffixes = humansize.SUFFIXES[1000]      1

3. >>> si_suffixes

4. ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

5. >>> '1000{0[0]} = 1{0[1]}'.format(si_suffixes)  2

6. '1000KB = 1MB'

1. Plutôt que d'appeler l'une des fonctions du module humanize, récupérons uniquement l'une de ses structures de données : la liste des suffixes « SI » (puissance de mille).

2. Cela semble compliqué, mais cela ne l'est pas. {0} ferait référence au premier argument de si_suffixes passé à la méthode format(). Mais si_suffixes est une liste. Donc, {0[0]} fait référence au premier item de la liste qui est le premier argument passé à la méthode format(): ‘KB’. De même, {0[1]} fait référence au second élément de la même liste : ‘MB’. Tout ce qui se trouve en dehors des accolades - incluant 1000, le signe égal ainsi que les espaces - est intact. Le résultat final est la chaîne de caractères ‘1000KB = 1MB’.

Cet exemple montre que les spécificateurs de format peuvent accéder aux éléments et propriétés des structures de données en utilisant (presque) la syntaxe Python. Ceci est appelé champs de noms composés. Les champs de noms composés suivants fonctionnent « tels quels » :

3. passer une liste et accéder à un élément de la liste par son index (comme dans l'exemple précédent) ;

4. passer un dictionnaire et accéder à une valeur du dictionnaire par une clef ;

5. passer un module et accéder à ses variables et fonctions par leurs noms ;

6. passer une instance de classe et accéder à ses propriétés et méthodes par leurs noms ;

7. toute combinaison des éléments ci-dessus.

Juste pour vous en mettre plein la vue, voici un exemple qui combine tous les points qui précèdent :

1. >>> import humansize

2. >>> import sys

3. >>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)

4. '1MB = 1000KB'

Voici comment cela fonctionne :

- le module sys contient des informations à propos de l'instance Python en cours. Comme vous venez juste de l'importer, vous pouvez passer le module sys lui-même en argument de la méthode format(). Ainsi le champ de remplacement {0} se réfère au module sys ;

- sys.modules est un dictionnaire de tous les modules qui ont été importés par cette instance Python. Les clefs sont les noms des modules en tant que chaîne de caractères ; les valeurs sont les objets des modules eux-mêmes. Donc le champ de remplacement {0.modules} fait référence au dictionnaire des modules importés ;

- sys.modules['humansize'] est le module humansize que vous venez tout juste d'importer. Le champ de remplacement {0.modules[humansize]} fait référence au module humansize. Notez ici la petite différence de syntaxe. En Python, les clefs du dictionnaire sys.modules sont des chaînes de caractères. Pour s'y référer, vous devez mettre des guillemets autour du nom du module (ex : 'humansize'). Mais dans un champ de remplacement, vous sautez les guillemets autour du nom de la clef du dictionnaire (ex : humansize). Pour citer la PEP 3101: Advanced String Formatting : « Les règles pour analyser une clef d'un item sont vraiment simples. Si elle débute avec un chiffre, alors elle est traitée comme un nombre, sinon elle est utilisée comme une chaîne de caractères. » ;

- sys.modules['humansize'].SUFFIXES est le dictionnaire défini en début du module humansize. Le champ de remplacement {0.modules[humansize].SUFFIXES}} fait référence au dictionnaire ;

- sys.modules['humansize'].SUFFIXES[1000] est la liste des suffixes SI: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']. Donc le champ de remplacement {0.modules[humansize].SUFFIXES[1000]} fait référence à la liste ;

- sys.modules['humansize'].SUFFIXES[1000][0] est le premier élément de la liste des suffixes SI : ‘KB’. Ainsi, le champ de remplacement complet {0.modules[humansize].SUFFIXES[1000][0]} est remplacé par la chaîne de deux caractères KB.

V-D-2 - Les spécificateurs de format

Mais attendez ! il y a plus ! regardons à nouveau cette étrange ligne de code du script humansize.py :

1. if size < multiple:

2. return '{0:.1f} {1}'.format(size, suffix)

{1} est remplacé par le deuxième argument transmis à la méthode format(), qui est suffix. Mais qu’est-ce que {0:.1f} ? Ce sont deux choses : {0}, que vous reconnaissez, et :.1f, que vous ne connaissez pas. La seconde moitié (les deux points y compris) définit le spécificateur de format, qui précise le formatage de la variable remplacée.

1. > Les spécificateurs de format vous permettent de transformer le texte de remplacement de différentes manières, à l'image de la fonction `printf()` en C. Vous pouvez ajouter des zéros ou des espaces, aligner des chaînes de caractères, contrôler la précision décimale et même convertir des nombres en hexadécimal.

Dans un champ de remplacement, les deux points (:) marquent le début du spécificateur de format. Le spécificateur de format ".1" signifie « arrondir au dixième près » (c'est-à-dire n'afficher qu'un chiffre après la décimale). Le spécificateur de format "f" signifie « nombre à virgule fixe(7) » (par opposition à la notation exponentielle ou toute autre représentation décimale). Ainsi, si la valeur de size était 698.24 et que celle de suffix était 'GB', alors, la chaîne formatée serait '698,2 GB', car '698,24' est arrondi à une décimale, puis le suffixe est ajouté après le nombre.

1. >>> '{0:.1f} {1}'.format(698.24, 'GB')

2. '698.2 GB'

Pour tous les détails croustillants sur les spécificateurs de format, consultez le chapitre « minilangage de spécification de format » de la documentation officielle de Python.

V-E - D'autres méthodes communes des chaînes de caractères

Outre le formatage, les chaînes de caractères peuvent faire beaucoup d'autres choses utiles.

1. >>> s = '''Finished files are the re-  1

2. ... sult of years of scientif-

3. ... ic study combined with the

4. ... experience of years.'''

5. >>> s.splitlines()                     2

6. ['Finished files are the re-',

7. 'sult of years of scientif-',

8. 'ic study combined with the',

9. 'experience of years.']

10. >>> print(s.lower())                   3

11. finished files are the re-

12. sult of years of scientif-

13. ic study combined with the

14. experience of years.

15. >>> s.lower().count('f')               4

16. 6

1. Dans le shell interactif Python, vous pouvez entrer des chaînes de caractères sur plusieurs lignes. Lorsque vous commencez une chaîne de caractères sur plusieurs lignes avec trois guillemets, pressez la touche ENTRÉE et le shell interactif vous invitera à poursuivre la chaîne. Tapez les trois guillemets fermants pour marquer la fin de la chaîne et au prochain ENTRÉE, la commande sera exécutée (dans le cas présent l'assignation de la chaîne de caractères à la variable s).

2. La méthode splitlines() prend une chaîne de caractères multilignes et renvoie une liste de chaînes, une pour chaque ligne de l'originale. Notez que le retour chariot n'est pas inclus.

3. La méthode lower() convertit la totalité de la chaîne de caractères en minuscules (de la même façon, la méthode upper() convertit la chaîne en majuscules).

4. La méthode count() compte le nombre d'occurrences d'une sous-chaîne de caractères. Oui, il y a réellement six « f » dans cette phrase !

Voici un autre cas classique. Disons que vous avez une liste de paires de clefs-valeurs de la forme key1=value1&key2=value2 et que vous souhaitez les séparer pour en faire un dictionnaire de la forme {key1: value1, key2: value2}.

1. >>> query = 'user=pilgrim&database=master&password=PapayaWhip'

2. >>> a_list = query.split('&')                                        1

3. >>> a_list

4. ['user=pilgrim', 'database=master', 'password=PapayaWhip']

5. >>> a_list_of_lists = [v.split('=', 1) for v in a_list if '=' in v]  2

6. >>> a_list_of_lists

7. [['user', 'pilgrim'], ['database', 'master'], ['password', 'PapayaWhip']]

8. >>> a_dict = dict(a_list_of_lists)                                   3

9. >>> a_dict

10. {'password': 'PapayaWhip', 'user': 'pilgrim', 'database': 'master'}

1. La méthode de chaîne split() requiert un argument, un séparateur. Cette méthode divise une chaîne de caractères en une liste de chaînes en se basant sur le séparateur. Ici, le séparateur est le caractère esperluette(8), mais cela pourrait être n'importe quel autre caractère.

2. Maintenant, nous avons une liste de chaînes de caractères, chacune avec une clef suivie par un signe égal, puis une valeur. Nous pouvons utiliser une liste en compréhension pour parcourir toute la liste et diviser chaque chaîne en deux chaînes en utilisant comme séparateur le premier signe égal. Le second argument, optionnel, de la méthode split() est le nombre de fois que vous souhaitez diviser. 1 signifie « diviser une seule fois », ainsi la méthode split() renverra une liste de deux éléments. (En théorie, une valeur pourrait aussi contenir un signe égal ; si vous faites 'key=value=foo'.split('='), vous obtiendrez une liste de trois éléments ['key', 'value', 'foo']).

3. Enfin, Python est en mesure de transformer cette liste de listes en un dictionnaire simplement en la passant en paramètre de la fonction dict().

L'exemple précédent ressemble beaucoup à l'analyse des paramètres d'une URL, mais en réalité, analyser une URL est bien plus compliqué. Si vous utilisez des paramètres de requête d'URL, il est préférable d'utiliser la fonction urllib.parse.parse_qs(), qui gère certains cas d'utilisation non évidents.

V-E-1 - Découper des chaînes de caractères

Une fois que vous avez défini une chaîne, vous pouvez récupérer n'importe quelle partie en tant que nouvelle chaîne. On appelle cela le découpage en tranches de chaînes. Le découpage de chaînes de caractères fonctionne exactement comme le découpage de listes, ce qui a du sens, car les chaînes sont juste des séquences de caractères.

1. >>> a_string = 'My alphabet starts where your alphabet ends.'

2. >>> a_string[3:11]           1

3. 'alphabet'

4. >>> a_string[3:-3]           2

5. 'alphabet starts where your alphabet en'

6. >>> a_string[0:2]            3

7. 'My'

8. >>> a_string[:18]            4

9. 'My alphabet starts'

10. >>> a_string[18:]            5

11. ' where your alphabet ends.'

1. Vous pouvez récupérer une partie de la chaîne, appelée une « tranche », en fournissant deux index. La valeur renvoyée est une nouvelle chaîne de caractères contenant tous les caractères de la chaîne, dans l'ordre, à partir du premier index.

2. Comme pour le découpage des listes, vous pouvez utiliser des index négatifs pour découper les chaînes.

3. Les chaînes de caractères commencent par l’index 0, ainsi a_string[0:2] renvoie les deux premiers éléments de la chaîne, commençant par a_string[0] jusqu'à a_string[2]mais sans l’inclure.

4. Si l’index de gauche est 0, vous pouvez ne pas l'indiquer, car 0 est implicite. Ainsi, a_string[:18] est équivalent à a_string[0:18].

5. De la même façon, si l’index de droite est de la longueur de la chaîne de caractères, vous pouvez ne pas l'indiquer. Ainsi, a_string[18:] est identique à a_string[18:44], car la chaîne de caractères est de 44 caractères. Il y a une symétrie plaisante ici. Dans cette chaîne de 44 caractères, a_string[:18] renvoie les 18 premiers caractères et a_string[18:] renvoie tous les caractères excepté les 18 premiers. En fait, a_string[:n] renverra toujours les n premiers caractères, et a_string[n:] renverra le reste, quelque soit la longueur de la chaîne.

V-F - Chaînes de caractères vs. octets

Les octets sont des octets ; les caractères sont une abstraction. Une séquence immuable de caractères Unicode est appelée une chaîne de caractères. Une séquence immuable de nombres entre 0 et 255 est appelée un objet bytes(9).

1. >>> by = b'abcd\x65'  1

2. >>> by

3. b'abcde'

4. >>> type(by)          2

5. <class 'bytes'>

6. >>> len(by)           3

7. 5

8. >>> by += b'\xff'     4

9. >>> by

10. b'abcde\xff'

11. >>> len(by)           5

12. 6

13. >>> by[0]             6

14. 97

15. >>> by[0] = 102       7

16. Traceback (most recent call last):

17. File "<stdin>", line 1, in <module>

18. TypeError: 'bytes' object does not support item assignment

1. Pour définir un objet bytes, utilisez la syntaxe b''10N.D.T. : byte literal en anglais . Chaque octet à l'intérieur de la syntaxe octet littéral peut être un caractère ASCII ou un nombre encodé en hexadécimal de \x00 à \xff (0-255).

2. Le type d'un objet bytes est bytes.

3. Comme pour les listes et les chaînes de caractères, vous pouvez déterminer la longueur de l'objet bytes en utilisant la fonction len().

4. Comme pour les listes et les chaînes de caractères, vous pouvez utiliser l'opérateur pour concaténer des objets bytes. Le résultat est un nouvel objet bytes.

5. Concaténer un objet bytes de 5 octets et un objet bytes d’un octet donne un objet bytes de 6 octets.

6. Comme pour les listes et les chaînes de caractères, vous pouvez récupérer un octet d'un objet bytes par son index. Les éléments d'une chaîne de caractères sont des chaînes de caractères ; les éléments d'un objet bytes sont des entiers. Plus précisément, les entiers compris entre 0 et 255.

7. Un objet bytes est immuable ; vous ne pouvez pas attribuer des octets individuels. Si vous avez besoin de changer des octets individuels, vous pouvez soit utiliser le découpage des chaînes de caractères et les opérateurs de concaténation (qui fonctionnent de la même façon que pour les chaînes de caractères), soit convertir les objets bytes en un objet bytearray.

1. >>> by = b'abcd\x65'

2. >>> barr = bytearray(by)  1

3. >>> barr

4. bytearray(b'abcde')

5. >>> len(barr)             2

6. 5

7. >>> barr[0] = 102         3

8. >>> barr

9. bytearray(b'fbcde')

1. Afin de convertir un objet bytes en un objet bytearray muable, utilisez la fonction intégrée bytearray().

2. Toutes les méthodes et opérations que vous pouvez faire sur les bytes peuvent également être faites sur les objets bytearray.

3. La seule différence est qu'avec l’objet bytearray, vous pouvez modifier les octets indépendamment en utilisant son index. La valeur assignée doit être comprise entre 0 et 255.

La seule chose que vous ne pouvez jamais faire est de mélanger les octets et les chaînes de caractères.

1. >>> by = b'd'

2. >>> s = 'abcde'

3. >>> by + s                       1

4. Traceback (most recent call last):

5. File "<stdin>", line 1, in <module>

6. TypeError: can't concat bytes to str

7. >>> s.count(by)                  2

8. Traceback (most recent call last):

9. File "<stdin>", line 1, in <module>

10. TypeError: Can't convert 'bytes' object to str implicitly

11. >>> s.count(by.decode('ascii'))  3

1. Vous ne pouvez pas concaténer des octets et des chaînes de caractères. Ce sont deux types de données différents.

2. Vous ne pouvez pas compter les occurrences des octets dans une chaîne de caractères, car il n'y a pas d'octets dans une chaîne. Une chaîne de caractères est une séquence de caractères. Peut-être voulez-vous « compter les occurrences de la chaîne que vous obtiendriez après le décodage de cette séquence d'octets dans un encodage de caractères particulier »? Eh bien, vous allez devoir le dire explicitement. Python 3 ne va pas implicitement convertir des octets en chaînes de caractères ou des chaînes en octets.

3. Par une étrange coïncidence, cette ligne de code dit « compter les occurrences de la chaîne que vous obtiendriez après le décodage de cette séquence d'octets dans un encodage de caractères particulier ».

Voici le lien entre les chaînes de caractères et les octets : les objets bytes ont une méthode decode() qui prend un encodage de caractères et renvoie une chaîne de caractères, et les chaînes de caractères ont une méthode encode() qui prend un encodage de caractères et renvoie un objet bytes. Dans l'exemple précédent, le décodage était relativement simple - convertir une séquence d'octets encodée en ASCII en une chaîne de caractères. Mais ce même procédé fonctionne avec n'importe quel encodage qui supporte les chaînes de caractères - même les anciens encodages (non Unicode).

1. >>> a_string = '深入 Python'         1

2. >>> len(a_string)

3. 9

4. >>> by = a_string.encode('utf-8')    2

5. >>> by

6. b'\xe6\xb7\xb1\xe5\x85\xa5 Python'

7. >>> len(by)

8. 13

9. >>> by = a_string.encode('gb18030')  3

10. >>> by

11. b'\xc9\xee\xc8\xeb Python'

12. >>> len(by)

13. 11

14. >>> by = a_string.encode('big5')     4

15. >>> by

16. b'\xb2`\xa4J Python'

17. >>> len(by)

18. 11

19. >>> roundtrip = by.decode('big5')    5

20. >>> roundtrip

21. '深入 Python'

22. >>> a_string == roundtrip

23. True

1. Ceci est une chaîne de caractères. Elle a neuf caractères.

2. Ceci est un objet bytes. Il est de 13 octets. C'est une séquence d'octets que vous obtenez lorsque vous prenez la chaîne a_string et que vous l'encodez en UTF-8.

3. Ceci est un objet bytes. Il est de 11 octets. C'est une séquence d'octets que vous obtenez lorsque vous prenez a_string et que vous l'encodez en GB18030.

4. Ceci est un objet bytes. Il est de 11 octets. C'est une séquence complètement différente d'octets que vous obtenez lorsque vous prenez a_string et que vous l'encodez en Big5.

5. C'est une chaîne de caractères. Elle a neuf caractères. C'est une séquence de caractères que vous obtenez lorsque vous prenez la chaîne by et que vous la décodez en utilisant l’algorithme d'encodage Big5. Elle est identique à la chaîne originale.

V-G - Postscriptum : encodage de caractères du code source de Python

Python 3 suppose que votre code source - c'est-à-dire chaque fichier .py - est encodé en UTF-8.

En Python 2, l'encodage par défaut pour les fichiers .py était l’ASCII. En Python 3, l'encodage par défaut est l'UTF-8.

Si vous souhaitez utiliser un encodage différent pour votre code Python, vous pouvez déclarer cet encodage sur la première ligne de chaque fichier. Cette déclaration spécifie qu’un fichier .py est encodé en windows-1252 :

1. # -*- coding: windows-1252 -*-

Techniquement, forcer l'encodage de caractères peut également se faire sur la seconde ligne, si la première est une commande shebang de type Unix.

1. #!/usr/bin/python3

2. # -*- coding: windows-1252 -*-

Pour plus d'informations, consultez la PEP 263 : Defining Python Source Code Encodings

V-H - Lectures complémentaires

À propos d'Unicode en Python :

- Python Guide Unicode

- What's New In Python 3: Text vs. Data Instead Of Unicode vs. 8-bit

- La PEP 261 explique comment Python gère les caractères du plan astral au-delà de la plage des caractères basiques multilingues (c.à.d, les caractères dont la valeur ordinale est plus grande que 65353)

À propos d'Unicode en général :

- The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

- On the Goodness of Unicode

- On Character Strings

- Characters vs. Bytes

À propos de l'encodage de caractères dans d'autres formats :

- Character encoding in XML

- Character encoding in HTML

À propos des chaînes et du formatage des chaînes de caractères :

- string — Opérations usuelles sur des chaînes

- Syntaxe de formatage de chaîne

- Minilangage de spécification de format

- PEP 3101 : formatage avancé des chaînes

VI - Chapitre 5 – Expressions Régulières

VI-A - PLONGER DANS

Extraire un petit bout de texte d’un grand bloc de texte est un défi. Avec Python, les chaînes de caractères ont des méthodes pour remplacer et rechercher : index(), find(), split(), count(), replace(), etc. Mais l’utilisation de ces méthodes est limitée aux cas simples. Par exemple, la méthode index() permet de chercher une seule sous-chaîne codée en dur, et la recherche est toujours sensible à la casse. Pour effectuer des recherches insensibles à la casse d'une chaîne s, vous devez appeler s.lower() ou s.upper() et vous assurer que vos chaînes de recherche correspondent à la casse appropriée. Les méthodes replace() et split() ont les mêmes limitations.

Si votre objectif peut être réalisé avec les méthodes de chaînes de caractères, vous devez les utiliser. Elles sont rapides et simples, et il y a beaucoup de choses à dire pour un code rapide, simple et lisible. Si vous vous trouvez en train d’utiliser plusieurs fonctions de chaînes de caractères avec des blocs de if pour gérer des cas particuliers ou si vous avez une suite d’appels de split() et de join() pour découper des chaînes de caractères, vous aurez peut-être besoin de passer aux expressions régulières.

Les expressions régulières sont une méthode puissante et souvent standardisée pour la recherche, le remplacement et le parcours d’un texte avec des motifs très compliqués. Quoique la syntaxe des expressions régulières soit rigide et ne ressemble pas à un code normal, le résultat peut finir par être plus lisible qu’une suite de fonctions des chaînes de caractères.

Il existe même des moyens d'inclure des commentaires dans des expressions régulières. Vous pouvez donc y ajouter une documentation affinée.

Si vous avez déjà utilisé des expressions régulières dans d’autres langages (tels que Perl, JavaScript, ou PHP), la syntaxe de Python sera familière. Lisez le résumé du module re « https://docs.python.org/dev/library/re.html#module-contents » pour avoir une idée générale des fonctions disponibles et leurs arguments.

VI-A-1 - Étude de cas : les adresses de rues

Cette série d'exemples est tirée d’un vrai problème vécu dans mon boulot cela fait des années, quand j’avais besoin de retraiter et normaliser les adresses des rues exportées d’un ancien système avant d’être importées au nouveau système. (Écoutez, je ne mets pas ce truc uniquement pour le mettre ; c’est vraiment utile.) Cet exemple montre comment j’ai abordé le problème.

>>>s = '100 NORTH MAIN ROAD'
>>>s.replace('ROAD', 'RD.')①
'100 NORTH MAIN RD.'
>>>s = '100 NORTH BROAD ROAD'
>>>s.replace('ROAD', 'RD.')②
'100 NORTH BRD. RD.'
>>>s[:-4] + s[-4:].replace('ROAD', 'RD.')③
'100 NORTH BROAD RD.'
>>>importre④
>>>re.sub('ROAD$', 'RD.', s)⑤
'100 NORTH BROAD RD.'

1. Mon objectif est de normaliser les adresses des rues de telle sorte que les routes 'ROAD' soient toujours abrégées en 'RD'. Heureusement, toutes les données étaient en majuscules, donc il n'y avait pas de soucis de la casse. La chaîne  'ROAD' à chercher était une constante. Du coup, l’utilisation de s.replace() dans cet exemple semble bien fonctionner.

2. Malheureusement, ce n’était pas aussi simple que cela, et je l’ai constaté rapidement. Le problème c’est que 'ROAD' apparaît deux fois dans une adresse, la première fois dans le nom de la rue 'BROAD' et la deuxième fois pour désigner la route 'ROAD'. En l'occurrence, la fonction replace() remplace les deux chaînes sans faire aucune distinction ; et voilà comment mes adresses ne ressemblaient plus à rien !

3. Pour résoudre ce problème de plusieurs sous-chaînes 'ROAD' dans une adresse, vous pouvez procéder ainsi : chercher et remplacer uniquement 'ROAD' dans les quatre caractères de l’adresse (s[-4:]), et laisser simplement la chaîne (s[:-4]). Mais comme vous le voyez, ce n’est pas évident de le faire. Par exemple, le motif dépend de la taille de la chaîne à remplacer. (Si vous remplaciez 'STREET' par 'ST.', vous utiliseriez s[: - 6] et s[-6:].replace(…).) Pourriez-vous revenir dans six mois et chercher où est le problème ? Personnellement, je ne le ferais pas.

4. C’est le temps d’aller vers les expressions régulières. En Python, le module re rassemble tout ce qui concerne les expressions régulières.

5. Prenons le premier paramètre : 'ROAD$'. C’est un exemple simple d’une expression régulière permettant d’identifier 'ROAD' à la fin d’une chaîne de caractères. Le $ signifie « la fin de la chaîne de caractères. » Il y a aussi le caractère lambda ^, qui permet d’identifier le début de la chaîne de caractères. En utilisant la fonction re.sub(), avec l’expression régulière 'ROAD$', on cherche le mot dans s et on le remplace par 'RD.'. Ceci permet de détecter 'ROAD' à la fin de la chaîne de caractères, mais non pas la sous-chaîne 'ROAD' dans 'BROAD', celle-ci étant au milieu de la chaîne s.

En avançant sur mon histoire de traitement des adresses, j’ai ensuite découvert que dans l’exemple précédent, chercher 'ROAD' à la fin de la chaîne n’était pas une bonne idée, car il y a des adresses qui ne désignent pas du tout la rue et se terminent simplement par son nom. Je m’en échappais, mais si la rue se termine par 'BROAD' comme nom, dans ce cas l’expression régulière identifiera 'ROAD' à la fin de la chaîne dans la sous-chaîne 'BROAD', ce que je ne souhaitais pas avoir.

^ reconnaît le début de la chaîne, $ reconnaît la fin de la chaîne.

1. >>>s = '100 BROAD'

2. >>>re.sub('ROAD$', 'RD.', s)

3. '100 BRD.'

4. >>>re.sub('\\bROAD$', 'RD.', s)   ①

5. '100 BROAD'

6. >>>re.sub(r'\bROAD$', 'RD.', s)   ②

7. '100 BROAD'

8. >>>s = '100 BROAD ROAD APT. 3'

9. >>>re.sub(r'\bROAD$', 'RD.', s)   ③

10. '100 BROAD ROAD APT. 3'

11. >>>re.sub(r'\bROAD\b', 'RD.', s)  ④

12. '100 BROAD RD. APT 3'

- Mon but était vraiment de trouver 'ROAD' quand il se situe à la fin de la chaîne et s’utilise pour indiquer le mot rue (et non pas la partie d’un autre mot). Pour décrire ceci dans une expression régulière, vous utilisez \b, qui veut dire « l’extrémité du mot doit être ici. » En Python, ceci n’est pas évident, car le caractère '\' en lui-même doit être ignoré. Il est parfois désigné comme un caractère antislash, et c’est l’une des raisons pour lesquelles les expressions régulières sont plus faciles en Perl qu’en Python. Mais d’un autre côté, Perl ne fait pas de distinction entre les expressions régulières et les autres syntaxes, du coup si vous avez un bogue, il vous sera difficile de savoir s'il vient de la syntaxe ou de l’expression régulière.

- Pour contourner le problème de l’antislash, vous pouvez utiliser ce que l’on appelle la chaîne 'raw', en mettant devant la chaîne de caractères le préfixe r. Ceci permet de dire à Python qu’il n'y a rien à échapper dans cette chaîne ; '\t' c’est le caractère de tabulation, mais le r'\t' est un antislash '\' suivi de la lettre t. Je recommande toujours l’utilisation de la chaîne 'raw' pour gérer les expressions régulières ; sinon les choses deviennent plus compliquées (alors que les expressions régulières ne manquent pas de complexité).

- Soupir ! Malheureusement, j’ai bientôt découvert pas mal de cas qui contredisent mon analyse. Dans le cas où l’adresse contient le mot entier 'ROAD', mais non pas dans la fin de l’adresse comme prévu, parce que l’adresse se termine par le numéro d’un appartement. Comme ilo n’est pas à la fin de la chaîne, il n’est pas reconnu, et donc l’appel de re.sub() ne remplace rien en fait, et vous avez toujours la chaîne initiale, ce que vous ne souhaitez pas.

- Pour pallier ce problème, j’ai supprimé le caractère $ et ajouté un autre \b. Maintenant l’expression régulière permet de « reconnaître le mot entier 'ROAD' n’importe où dans la chaîne, » que ce soit à la fin, au début, ou quelque part au milieu.

VI-B - Case Study: Roman Numerals

Vous avez sûrement vu des chiffres romains, même si vous ne les avez pas reconnus. Peut-être que vous les avez vus dans les anciens films et les émissions télévisées (« Copyright MCMXLVI » au lieu de « Copyright 1946 »), ou sur les murs de dédicace des bibliothèques ou des universités (« established MDCCCLXXXVIII » au lieu de « established 1888 »). Vous pouvez aussi les avoir vus dans les plans et références de bibliothèques. C’est un système de représentation des nombres qui date vraiment de l’ancien Empire romain (d’où le nom). Dans la numérotation romaine, il y a sept caractères qui se répètent et se combinent de différentes manières pour représenter les nombres.

- I = 1

- V = 5

- X = 10

- L = 50

- C = 100

- D = 500

- M = 1000

Voici quelques règles générales pour constituer des nombres romains :

- parfois les caractères s’additionnent : I est 1, II est 2, et III est 3. VI est 6 (littérairement, « 5 et 1 »), VII est 7 et VIII est 8 ;

- les caractères des dizaines (I, X, C, and M) peuvent se répéter jusqu’à trois fois. Pour 4, il vous faudra soustraire du plus grand prochain caractère. Vous ne pouvez pas représenter 4 comme IIII ; par contre, il est représenté comme IV (« 1 soustrait de 5 »). 40 est représenté par XL (« 10 soustrait de 50 »), 41 par XLI, 42 par XLII, 43 par XLIII, et 44 par XLIV (« 10 soustrait de 50, ensuite 1 soustrait de 5 ») ;

- parfois les caractères ne s’additionnent pas. En mettant certains caractères devant les autres, vous soustrairez de la valeur finale. Par exemple, pour 9, vous devez soustraire du plus grand prochain caractère : 8 est VIII, mais 9 est IX (« 1 soustrait de 10 »), et non VIIII (puisque le caractère I ne peut se répéter quatre fois). 90 est XC, 900 est CM ;

- le caractère 5 ne peut pas être répété. 10 est toujours représenté par X, jamais par VV. 100 est toujours représenté par C, jamais LL ;

- les nombres romains sont lus de gauche à droite, du coup l’ordre des caractères compte beaucoup. DC est 600 ; CD est un nombre totalement différent (400, « 100 soustrait de 500 »). CI est 101 ; IC n’est même pas un nombre romain valide (parce que vous ne pouvez pas soustraire 1 directement de 100 ; vous devez écrire XCIX, « 10 soustrait de 100, ensuite 1 soustrait de 10 »).

VI-B-1 - Recherche de milliers

Qu’est-ce qu’il faudrait faire pour dire qu’une quelconque chaîne de caractères correspond à un nombre romain ? Prenons cela chiffre par chiffre. Comme les nombres romains sont toujours écrits du plus grand au plus petit, nous allons alors commencer par le plus grand : les milliers. Pour les nombres 1000 et plus, les milliers sont représentés par une suite de caractères M.

1. >>>import re

2. >>>pattern = '^M?M?M?$'        ①

3. >>>re.search(pattern, 'M')     ②

4. <re.Match object; span=(0, 1), match='M'>

5. >>>re.search(pattern, 'MM')    ③

6. <re.Match object; span=(0, 2), match='MM'>

7. >>>re.search(pattern, 'MMM')   ④

8. <re.Match object; span=(0, 3), match='MMM'>

9. >>>re.search(pattern, 'MMMM')  ⑤

10. >>>re.search(pattern, '')      ⑥

11. <re.Match object; span=(0, 0), match=''>

1. Ce motif est constitué de trois parties. ^ reconnaît uniquement ce qui suit au début de la chaîne. Si ce caractère n’est pas spécifié, le caractère M sera identifié n’importe où dans la chaîne, ce que vous ne souhaitez pas. Vous voulez être sûr que les caractères M, s’il y en a, sont au début de la chaîne. M? optionnellement reconnaît un seul caractère M. Puisqu'il est répété trois fois, vous identifiez le caractère de zéro à trois fois dans la chaîne. Le $ reconnaît la fin de la chaîne. Mais combiné avec le caractère ^ en début, cela signifie que le motif doit identifier la suite des caractères M, sans aucun autre caractère avant ou après.

2. L’essence du module re est la fonction search(), qui prend l’expression régulière (le motif) et le caractère ('M') afin d’identifier le motif spécifié. Si la recherche aboutit, la fonction search() retourne un objet ayant plusieurs méthodes décrivant le résultat de la recherche. Si la recherche n’aboutit pas, la fonction search() retourne None, la valeur nulle de Python. Tout ce qui vous intéresse pour le moment c’est si la recherche aboutit, ce que vous pouvez détecter en regardant juste la valeur de retour de search().'M' est reconnue par cette expression régulière, car le premier optionnel M est identifié et le deuxième et le troisième optionnels M sont ignorés.

3. 'MM' est reconnue, car le premier et le deuxième optionnels M sont identifiés et le troisième optionnel M est ignoré.

4. 'MMM' est reconnue, car tous les trois caractères M sont identifiés.

5. 'MMMM' n’est pas reconnue. Les trois caractères M sont identifiés, mais ensuite, l’expression régulière insiste sur la fin de la chaîne (grâce au caractère $), et la chaîne ne se termine pas comme souhaité (à cause du quatrième M). Ainsi search() retourne None.

6. De manière très intéressante, une chaîne vide est aussi reconnue par cette expression régulière, comme tous les M sont optionnels.

VI-B-2 - Vérification des centaines

La recherche des centaines est beaucoup plus difficile que celle des milliers, car il y a plusieurs manières de les représenter qui dépendent de leur propre valeur.

? rendre un motif optionnel.

- 100 = C

- 200 = CC

- 300 = CCC

- 400 = CD

- 500 = D

- 600 = DC

- 700 = DCC

- 800 = DCCC

- 900 = CM

Ainsi il est possible d’utiliser quatre motifs :

- CM ;

- CD ;

- Zéro à trois caractères C (zéro s’il n'y en a pas) ;

- D, suivi de zéro à trois caractères C.

Les deux derniers motifs peuvent être combinés :

- un D optionnel, suivi de zéro à trois caractères C.

Cet exemple montre comment valider l’emplacement des centaines dans les nombres romains.

1. >>>import re

2. >>>pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'①

3. >>>re.search(pattern, 'MCM')②

4. <re.Match object; span=(0, 3), match='MCM'>

5. >>>re.search(pattern, 'MD')③

6. <re.Match object; span=(0, 2), match='MD'>

7. >>>re.search(pattern, 'MMMCCC')④

8. <re.Match object; span=(0, 6), match='MMMCCC'>

9. >>>re.search(pattern, 'MCMC')⑤

10. >>>re.search(pattern, '')⑥

11. <re.Match object; span=(0, 0), match=''>

1. Ce motif procède au début comme le précédent, en vérifiant le début de la chaîne (^), et l’emplacement des milliers (M?M?M?). Ensuite il contient la nouvelle partie, entre parenthèses, qui définit un ensemble de trois motifs, séparés par des barres verticales : CM, CD, et D?C?C?C? (qui correspond à un D optionnel, suivi de zéro à trois caractères C). Le parcours de l’expression régulière permet de reconnaître dans l’ordre (de gauche à droite) tous ces motifs, de considérer le premier identifié et ignorer le reste.

2. 'MCM' reconnue, car le premier M est identifié, le deuxième et le troisième M sont ignorés, et la chaîne CM reconnue (du coup les motifs CD et D?C?C?C? ne seront jamais évalués). MCM est le nombre romain représentant 1900,

3. 'MD' reconnue, car le premier M est identifié, le deuxième et le troisième M sont ignorés, et le motif D?C?C?C? identifie le D (tous les caractères C sont optionnels et donc ignorés). MD est le nombre romain représentant 1500.

4. 'MMMCCC' est reconnue, car tous les trois caractères M sont identifiés, et le motif D?C?C?C? identifie CCC (le D est optionnel et donc ignoré). MMMCCC est le nombre romain représentant 3300.

5. 'MCMC' n’est pas reconnue. Le premier caractère M est identifié, le deuxième et le troisième M sont ignorés, mais ensuite le $ n’identifie rien, car vous n’êtes pas encore arrivé à la fin de la chaîne (vous avez toujours un caractère C qui n’est pas reconnu). Le C n’est pas évalué par le motif D?C?C?C?, car le motif CM est déjà identifié.

6. De manière très intéressante, une chaîne vide est aussi reconnue par cette expression régulière, parce que tous les M sont optionnels et ignorés et le motif D?C?C?C?, où tous les caractères sont optionnels et ignorés, reconnaît la chaîne vide.

Ouf ! Regardez comment les expressions régulières peuvent rapidement devenir compliquées ? Et vous n’avez couvert que la vérification des milliers et des centaines. Mais si vous avez suivi jusqu’ici, l’identification des dizaines et des unités est simple, car elle est faite exactement par le même motif. Mais allons regarder une autre manière d’exprimer le motif.

VI-C - Utilisation de la syntaxe {n, m}

{1,4} reconnaît entre 1 à 4 occurrences du motif.

Dans la section précédente, vous avez utilisé des motifs où un caractère peut se répéter jusqu’à trois fois. Il y a un autre moyen d’exprimer cela dans les expressions régulières, que quelques-uns trouvent plus lisible. D’abord, regardons la première méthode que nous avons utilisée dans l’exemple précédent.

1. >>>import re

2. >>>pattern = '^M?M?M?$'

3. >>>re.search(pattern, 'M')①

4. <re.Match object; span=(0, 1), match='M'>

5. >>>re.search(pattern, 'MM')②

6. <re.Match object; span=(0, 2), match='MM'>

7. >>>re.search(pattern, 'MMM')③

8. <re.Match object; span=(0, 3), match='MMM'>

9. >>>re.search(pattern, 'MMMM')④

10. >>>

1. Il reconnaît le début de la chaîne, et ensuite le premier optionnel M, mais ignore le deuxième et troisième M (mais ce n’est pas méchant, car ils sont optionnels), puis la fin de la chaîne.

2. Il reconnaît le début de la chaîne, et ensuite le premier et deuxième M optionnels, mais ignore le troisième M (mais ce n’est pas méchant, car il est optionnel), puis la fin de la chaîne.

3. Il reconnaît le début de la chaîne, et ensuite tous les M optionnels, puis la fin de la chaîne.

4. Il reconnaît le début de la chaîne, et ensuite tous les M optionnels, mais ne reconnaît pas la fin de la chaîne (car il y a encore un M qui n’est pas identifié), du coup le motif ne correspond pas à ce que l’on cherche et retourne None.

>>> import re
>>> pattern = '^M{0,3}$'  ①
>>> re.search(pattern, 'M') ②
<re.Match object; span=(0, 1), match='M'>
>>> re.search(pattern, 'MM') ③
<re.Match object; span=(0, 2), match='MM'>
>>> re.search(pattern, 'MMM') ④
<re.Match object; span=(0, 3), match='MMM'>
>>> re.search(pattern, 'MMMM') ⑤
>>>

① Ce modèle dit : « Faites correspondre le début de la chaîne, puis de zéro à trois M caractères, puis la fin de la chaîne. » Le 0 et le 3 peuvent être n'importe quel nombre ; si vous voulez faire correspondre au moins un, mais pas plus de trois caractères M, vous pouvez dire M {1,3}.

② Cela correspond au début de la chaîne, puis à un M sur trois possibles, puis à la fin de la chaîne.

③ Cela correspond au début de la chaîne, puis deux M sur trois possibles, puis à la fin de la chaîne.

④ Cela correspond au début de la chaîne, puis trois M sur trois possibles, puis la fin de la chaîne.

⑤ Cela correspond au début de la chaîne, puis trois M sur trois possibles, mais ne correspond pas à la fin de la chaîne. L'expression régulière n'autorise que trois caractères M avant la fin de la chaîne, mais vous en avez quatre, de sorte que le modèle ne correspond pas et renvoie Nul.

VI-C-1 - Vérification des dizaines et des unités

1. Maintenant, étalons l’expression régulière d’identification des nombres romains pour couvrir les dizaines et unités également. Cet exemple montre la vérification des dizaines.

1. >>>pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'

2. >>>re.search(pattern, 'MCMXL')①

3. <re.Match object; span=(0, 5), match='MCMXL'>

4. >>>re.search(pattern, 'MCML')②

5. <re.Match object; span=(0, 4), match='MCML'>

6. >>>re.search(pattern, 'MCMLX')③

7. <re.Match object; span=(0, 5), match='MCMLX'>

8. >>>re.search(pattern, 'MCMLXXX')④

9. <re.Match object; span=(0, 7), match='MCMLXXX'>

10. >>>re.search(pattern, 'MCMLXXXX')⑤

11. >>>

2. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et L?X?X?X?. Dans L?X?X?X?, il reconnaît le L et ignore les trois caractères X optionnels. Ensuite, aller à la fin de la chaîne. MCMXL est le nombre romain représentant 1950.

3. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et il reconnaît le L, le premier M optionnel et ignore les deux caractères X optionnels, ensuite la fin de la chaîne MCMLX est le nombre romain représentant 1960.

4. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et il reconnaît le L et les trois caractères X optionnels, ensuite la fin de la chaîne. MCMLXXX est le nombre romain représentant 1980

5. Ce motif identifie le début de la chaîne, le premier M optionnel, CM et il reconnaît le L et les trois caractères X optionnels, mais ne réussit pas à détecter la fin de la chaîne, car il y a toujours un x non identifié et du coup la recherche retourne None. MCMLXXXX n’est pas un nombre romain.

1. Ce motif identifie le début de la chaîne, le premier M optionnel, CM, XL et la fin de la chaîne. Rappelons que la syntaxe (A|B|C) signifie « détecter exactement A ou B ou C ». Vous détectez XL, donc vous ignorez XC et les choix L?X?X?X?, ensuite allez à la fin de la chaîne. MCMXL est le nombre romain représentant 1940.

(A|B) reconnaît soit A soit B mais jamais les deux à la fois.

La recherche des unités suit le même raisonnement. Je ne rentrerai pas dans les détails et vous montrerai le résultat final.

1. >>>pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

Qu'est-ce que cela va devenir en utilisant cette syntaxe {n,m} ? Cet exemple montre la nouvelle syntaxe.

1. >>>pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'

2. >>>re.search(pattern, 'MDLV')①

3. <re.Match object; span=(0, 4), match='MDLV'>

4. >>>re.search(pattern, 'MMDCLXVI')②

5. <re.Match object; span=(0, 8), match='MMDCLXVI'>

6. >>>re.search(pattern, 'MMMDCCCLXXXVIII')③

7. <re.Match object; span=(0, 15), match='MMMDCCCLXXXVIII'>

8. >>>re.search(pattern, 'I')④

9. <re.Match object; span=(0, 1), match='I'>

1. Ce motif reconnaît le début de la chaîne, après l’un des trois possibles M, ensuite D?C{0,3}. Dans ce dernier, il reconnaît le D optionnel et l’un des trois possibles C. En continuant, il évalue L?X{0,3} en détectant le L et l’un des trois possibles X. Ensuite, il évalue V?I{0,3} en détectant le V et l’un des trois possibles I, et finalement la fin de la chaîne. MDLV est le nombre romain représentant 1555.

2. Ce motif reconnaît le début de la chaîne, après l’un des deux M possibles, après D?C{0,3} avec un D et l’un des trois possibles C ; ensuite L?X{0,3} avec un L et l’un des trois X possibles, et V?I{0,3} avec un V et l’un des trois possibles I, et finalement la fin de la chaîne. MMDCLXVI est le nombre romain représentant 2666.

3. Ce motif reconnaît le début de la chaîne, après l’un des trois possibles M, après D?C{0,3} avec un D et tous les trois possibles C ; ensuite L?X{0,3} avec un L et tous les trois possibles X, et V?I{0,3} avec un V, et tous les trois possibles I, et finalement la fin de la chaîne. MMMDCCCLXXXVIII est le nombre romain représentant 3888. C’est le nombre le plus long des nombres romains que vous pouvez rencontrer.

4. Regardez attentivement. (J’ai l’impression que je fais un tour de magie. « Les enfants, regardez, je vais faire sortir un lapin de mon chapeau »). Ce motif reconnaît le début de la chaîne, après aucun des trois M possibles, après D?C{0,3} en ignorant D et en ne détectant aucun C des trois C possibles, après L?X{0,3} en ignorant L et ne détectant aucun des trois X possibles, après V?I{0,3} en ignorant V et détectant un I parmi les trois I possibles, et finalement la fin de la chaîne. Whoa !

Si vous avez suivi tout cela et l’avez compris dès la première fois, vous êtes mieux que moi. Maintenant, imaginez le fait de comprendre une expression régulière d’une autre personne, dans une fonction faisant partie d’un gros programme. Ou simplement, imaginez que vous reviendrez vers votre propre expression régulière quelques mois après. Je l’ai fait, et ce n’est pas du tout sympa.

Alors, allons explorer une nouvelle syntaxe permettant d’avoir une expression maintenable.

VI-C-2 - Expressions régulières verbeuses

Au début vous étiez en train de manipuler ce que j’appelle des expressions régulières « compactes ». Comme vous l’avez vu, elles sont difficilement lisibles, et même si vous comprenez ce qui était fait, rien ne vous assure que dans six mois vous le comprendrez toujours. Ce dont vous aurez vraiment besoin c’est d’une documentation en ligne.

Python vous donne cette possibilité en utilisant ce que l’on appelle les expressions régulières détaillées « verbose ». Les expressions régulières détaillées se distinguent des expressions régulières compactes par deux choses :

- tous les types d’espaces sont ignorés. Les espaces, tabulations et les retours chariots ne sont pas considérés comme des espaces, des tabulations et des retours chariot. Ils ne sont pas pris en considération du tout. (Pour reconnaître une espace dans une expression régulière détaillée, vous devez la déspécifier en la précédant d'un antislash.)

Les commentaires sont ignorés. Le commentaire dans l’expression régulière détaillée est exactement comme le commentaire dans le code Python : il commence par le caractère # et se termine à la fin de la ligne. Dans ce cas, le commentaire est inséré dans une chaîne multiligne au lieu d’un commentaire pour une ligne de code, mais cela fonctionne de la même manière.

Cela va être plus clair avec un exemple. Revenons sur l’expression régulière compacte avec laquelle vous avez déjà travaillé, et transformons-la en une expression régulière détaillée. L’exemple suivant explique comment le faire.

1. >>>pattern = '''

2. ^                   # début de la chaîne

3. M{0,3}              # milliers - 0 to 3 Ms

4. (CM|CD|D?C{0,3})    # centaines - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),

5. #            or 500-800 (D, suivi de 0 à 3 Cs)

6. (XC|XL|L?X{0,3})    # dizaines - 90 (XC), 40 (XL), 0-30 (0 à 3 Xs),

7. #        or 50-80 (L, suivi de 0 à 3 Xs)

8. (IX|IV|V?I{0,3})    # unités - 9 (IX), 4 (IV), 0-3 (0 à 3 Is),

9. #        or 5-8 (V, suivi de 0 à 3 Is)

10. $                   # fin de la chaîne

11. '''

12. >>>re.search(pattern, 'M', re.VERBOSE)                 ①

13. <re.Match object; span=(0, 1), match='M'>

14. >>>re.search(pattern, 'MCMLXXXIX', re.VERBOSE)         ②<re.Match object; span=(0, 9), match='MCMLXXXIX'>

15. >>>re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)   ③

16. <re.Match object; span=(0, 15), match='MMMDCCCLXXXVIII'>

17. >>>re.search(pattern, 'M')                             ④

18. >>>

1. Le plus important à retenir avec les expressions régulières détaillées, c’est l’argument extra re.VERBOSE qui est une constante définie dans le module re indiquant que le motif doit être traité comme expression régulière détaillée. Comme vous pouvez le voir, le motif contient plusieurs espaces (qui sont toutes ignorées) et plusieurs commentaires (qui sont tous ignorés). Une fois que vous ignorez toutes les espaces et tous les commentaires, vous tombez exactement sur la même expression régulière que vous avez dans la section précédente, mais beaucoup plus lisible.

2. Ce motif identifie le début de la chaîne et un parmi les trois caractères M optionnels, ensuite CM, ensuite L et trois sur les trois caractères X, ensuite IX, puis la fin de la chaîne.

3. Ce motif identifie le début de la chaîne et trois sur les trois caractères M optionnels, ensuite D et trois sur les trois caractères C, L et trois sur les trois caractères X, V et trois sur les trois caractères I, puis la fin de la chaîne.

4. Ce motif ne reconnaît rien. Pourquoi ? Parce que l’argument re.VERBOSE n’est pas passé, du coup la fonction re.search traite le motif comme une expression régulière compacte, en prenant en considération les espaces et les commentaires. Python ne détecte pas si l’expression régulière est détaillée systématiquement ou non. Par défaut, il considère toute expression régulière compacte sauf si vous spécifiez explicitement qu’elle est détaillée.

VI-C-3 - Étude de cas : analyse des numéros de téléphone

\d reconnaît tous les numéros de (0–9), \D reconnaît tout sauf les numéros.

Avant vous étiez concentré sur l’identification du motif tout entier. Que le motif soit reconnu ou non. Mais les expressions régulières sont plus puissantes que cela. Quand une expression régulière est reconnue, vous pouvez récupérer des informations plus spécifiques. Vous pouvez savoir qu’est-ce qui a été reconnu et où.

Cet exemple vient d’un vrai problème vécu dans mon ancien boulot. Le problème de parcours d’un répertoire téléphonique américain. Le client souhaitait pouvoir entrer le numéro sous forme libre (en un seul champ), mais souhaitait ensuite stocker l’indicatif, la ligne réseau, le numéro et éventuellement une extension dans la base de données de la société. J’ai cherché sur Internet et trouvé un ensemble des expressions régulières supposées répondre à ce problème, mais aucune d’entre elles n’était efficace.

Ci-dessous les numéros de téléphone que j’ai besoin de gérer :

- 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

Pas mal de choix ! Pour tous ces cas, je dois savoir que l’indicatif est 800, la ligne est 555 et le reste du numéro de téléphone est 1212. Pour ceux contenant une extension, je devais savoir que l’extension est 1234.

Nous allons travailler sur une solution pour parcourir un répertoire téléphonique. Cet exemple montre la première étape.

1. >>>phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  ①

2. >>>phonePattern.search('800-555-1212').groups()             ②

3. ('800', '555', '1212')

4. >>>phonePattern.search('800-555-1212-1234')                 ③

5. >>>phonePattern.search('800-555-1212-1234').groups()        ④

6. Traceback (mostrecent call last):

7. File "<stdin>", line 1, in <module>

8. AttributeError: 'NoneType' object has no attribute 'groups'

1. Il faut toujours lire les expressions régulières de gauche à droite. Ce motif reconnaît le début de la chaîne, ensuite (\d{3}). À quoi correspond \d{3} ? Bon, \d correspond à « tout caractère numérique » (0 à 9). Le {3} permet de « reconnaître exactement trois caractères numériques » ; c’est une alternative de la syntaxe {n,m} que vous avez déjà vue. Mettre tout entre parenthèses permet de « reconnaître exactement trois numériques, et ensuite les mémoriser sous forme d’un groupe que je reviendrai récupérer ensuite ». Ensuite un tiret et reconnaître exactement trois numériques. Ensuite un tiret et reconnaître exactement quatre numériques, puis reconnaître la fin de la chaîne.

2. Pour accéder aux groupes dont l’expression régulière se souvient en parcourant la chaîne, utilisez la méthode groups() avec l’objet retourné par la méthode search(). Elle va retourner un tuple de groupes, peu importe où ils sont définis dans l’expression régulière. Dans ce cas vous avez trois groupes, le premier et le deuxième avec trois numériques et le dernier avec quatre numériques.

3. Cette expression régulière ne répond pas à tous les groupes, car il ne gère pas le cas d’un numéro de téléphone avec une extension à la fin. Pour ceci, on doit étendre notre expression régulière.

4. Et c’est pour cela qu’il ne faut jamais utiliser les méthodes search() et groups() ensemble. Si search() ne reconnaît rien, elle retourne None, et non un objet reconnu par l’expression régulière. Appeler None.groups() produit parfaitement une exception : None doesn’t have a groups() method. (Certes, c’est un peu moins évident quand vous obtiendrez cette erreur dans un code plus compliqué. Oui, là je parle vraiment de mon expérience.)

1. >>>phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  ①

2. >>>phonePattern.search('800-555-1212-1234').groups()              ②

3. ('800', '555', '1212', '1234')

4. >>>phonePattern.search('800 555 1212 1234')                       ③

5. >>>

6. >>>phonePattern.search('800-555-1212')                            ④

7. >>>

1. Cette expression régulière est quasiment identique à la première. Comme avant, vous reconnaissez le début de la chaîne, ensuite mémorisez le groupe de trois caractères numériques, puis un tiret, mémorisez le groupe de trois caractères numériques, puis un tiret, et mémorisez un groupe de quatre caractères numériques. Ce qui est nouveau c’est que vous reconnaissez un autre tiret et, ensuite mémorisez un groupe de trois caractères numériques, puis la fin de la chaîne.

2. Comme l’expression régulière définit quatre groupes, la méthode groups() retourne maintenant un tuple de quatre éléments.

3. Malheureusement, cette expression régulière n’est pas non plus la réponse finale au problème, car elle suppose que les différentes parties du numéro du téléphone sont séparées par des tirets. Et si elles sont séparées par des espaces, des virgules ou des points ? Vous aurez besoin d’une solution plus générale afin de reconnaître plusieurs types de séparateurs.

4. Oups ! Non seulement cette expression régulière ne fait pas ce que l’on souhaite, mais de plus, c’est un pas en arrière, parce que maintenant vous ne pouvez plus parcourir un numéro de téléphone ne contenant pas une extension. Ce n’est pas du tout ce que l’on veut faire ; si l’extension existe, on veut la reconnaître, mais si elle n’existe pas nous voulons toujours savoir les autres différentes parties du numéro de téléphone.

L’exemple suivant montre l’expression régulière pour gérer les séparateurs entre les autres différentes parties du numéro de téléphone.

1. >>>phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  ①

2. >>>phonePattern.search('800 555 1212 1234').groups()  ②

3. ('800', '555', '1212', '1234')

4. >>>phonePattern.search('800-555-1212-1234').groups()  ③

5. ('800', '555', '1212', '1234')

6. >>>phonePattern.search('80055512121234')              ④

7. >>>

8. >>>phonePattern.search('800-555-1212')                ⑤

9. >>>

1. Accrochez-vous. Vous reconnaissez le début de la chaîne, ensuite un groupe de trois numéros, ensuite \D+. Mais c’est quoi ce truc ? Bon, \D reconnaît tout caractère sauf un caractère numérique et + spécifie « 1 ou plus ». Du coup \D+ reconnaît un ou plusieurs caractères qui ne sont pas des numériques. C’est ce qu’il faut utiliser au lieu du tiret pour pouvoir reconnaître les différents types de séparateurs.

2. Utiliser \D+ au lieu de - permet de reconnaître les numéros de téléphone dont les différentes parties sont séparées par une espace au lieu du tiret.

3. Bien évidemment cela fonctionnera aussi avec les tirets.

4. Malheureusement, ceci n’est toujours pas la réponse finale, parce que cela suppose qu’il y a forcément un séparateur. Qu’est-ce qu’il en serait si le numéro était saisi sans tirets ni espaces ?

5. Oups ! Ceci ne résout pas le problème des extensions non plus. Maintenant on a deux problèmes, mais on peut pallier les deux avec la même technique.

L’exemple suivant montre l’expression régulière pour gérer les numéros de téléphone sans séparateurs.

1. >>>phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①

2. >>>phonePattern.search('80055512121234').groups()      ②

3. ('800', '555', '1212', '1234')

4. >>>phonePattern.search('800.555.1212 x1234').groups()  ③

5. ('800', '555', '1212', '1234')

6. >>>phonePattern.search('800-555-1212').groups()        ④

7. ('800', '555', '1212', '')

8. >>>phonePattern.search('800-555-1212').groups()           ⑤

9. >>>

1. Le seul changement qui a été fait c’est d’avoir changé tous les + par *. Au lieu de \D+ entre les parties du numéro de téléphone, nous reconnaissons maintenant \D*. Vous vous rappelez que + veut dire « 1 ou plus » ? Bon, * veut dire « zéro ou plus ». Alors maintenant vous serez capable de parcourir les numéros de téléphone même quand ils ne contiennent pas de séparateurs.

2. Mais voilà, cela fonctionne bien. Pourquoi ? Vous reconnaissez le début de la chaîne ensuite un groupe de trois numéros (800), puis zéro caractère non numérique, un groupe de trois numéros (555), zéro caractère non numérique, un groupe de quatre numéros (1212), zéro caractère non numérique, un groupe de quatre numéros (1234), et finalement la fin de la chaîne.

3. D’autres combinaisons fonctionnent aussi : des points au lieu de tirets, et pour chacun d'eux une espace ou un x précédant une extension.

4. Et finalement, vous avez résolu le problème des extensions ; elles sont de nouveau facultatives. Si on ne trouve pas d’extension, la méthode groups() retourne toujours un tuple de quatre éléments, mais le quatrième est tout simplement une chaîne vide.

5. Je n’aime pas être le porteur de mauvaises nouvelles, mais vous n’avez pas encore fini. Quel est le problème ici ? Il y a un caractère de plus précédant l’indicatif. Mais l’expression régulière suppose que l’indicatif soit toujours le début de la chaîne. Pas de problème, vous pouvez utiliser la même technique du “zéro caractère non numérique ou plus” pour se passer du caractère précédant l’indicatif.

L’exemple suivant montre comment gérer le caractère précédant l’indicatif dans le numéro du téléphone.

1. >>>phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①

2. >>>phonePattern.search('(800)5551212 ext. 1234').groups()                  ②

3. ('800', '555', '1212', '1234')

4. >>>phonePattern.search('800-555-1212').groups()                            ③

5. ('800', '555', '1212', '')

6. >>>phonePattern.search('work 1-(800) 555.1212 #1234')                      ④

7. >>>

1. C’est la même chose que l’exemple précédent, sauf que maintenant vous reconnaissez \D*, zéro ou plus d’un caractère non numérique, avant le premier groupe mémorisé (l’indicatif). Il faut noter que vous ne mémorisez pas ce caractère non numérique (il n’est pas mis entre parenthèses). Si vous le trouvez, vous allez l'ignorer et ensuite commencer à mémoriser l’indicatif une fois et vous y arrivez.

2. Vous pouvez brillamment parcourir le numéro de téléphone, même s'il y a une parenthèse avant l’indicatif. (La parenthèse fermante qui vient après l’indicatif est déjà réglée, elle est traitée comme un séparateur non numérique et identifiée par le \D* après le premier groupe mémorisé.)

3. Juste une vérification pour s’assurer que vous n’avez pas altéré quelque chose qui fonctionnait bien. Comme le premier caractère est totalement optionnel, cela reconnaît le début de la chaîne, ensuite zéro caractère non numérique, puis le groupe mémorisé de trois caractères (800), un caractère non numérique (la virgule), le groupe mémorisé de trois caractères (555), un caractère non numérique (la virgule), le groupe mémorisé de quatre caractères (1212), zéro caractère non numérique, le groupe mémorisé de zéro numéro, et finalement la fin de la chaîne.

4. C’est là où les expressions régulières m’ont poussé à vouloir m’arracher les yeux. Pourquoi ce numéro de téléphone n’est pas reconnu ? Parce qu'il y a un 1 avant l’indicatif, mais vous avez supposé que tous les caractères au début allaient être des non numériques (\D*). Aargh !

Revenons dessus pour une seconde. Avant l’expression régulière reconnaissait tout depuis le début de la chaîne. Mais maintenant vous réalisez que la chaîne peut commencer par un nombre indéterminé de choses que vous devez ignorer. Mis à part essayer de les reconnaître tous, vous devez aussi pouvoir les ignorer, on va adopter une nouvelle approche : ne pas reconnaître le début de la chaîne explicitement. Le deuxième exemple montre cette approche.

1. >>>phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  ①

2. >>>phonePattern.search('work 1-(800) 555.1212 #1234').groups()         ②

3. ('800', '555', '1212', '1234')

4. >>>phonePattern.search('800-555-1212').groups()                        ③

5. ('800', '555', '1212', '')

6. >>>phonePattern.search('80055512121234').groups()                      ④

7. ('800', '555', '1212', '1234')

1. Il faut noter le manque du ^ au début de l’expression régulière. Vous ne reconnaissez plus le début de la chaîne. Il n’y a rien qui spécifie que vous devez reconnaître la totalité de votre entrée avec votre expression régulière. Le moteur de l’expression régulière fera l’effort d’identifier où se trouve le début de ce qu’il faudrait reconnaître et partir dessus.

2. Maintenant vous pouvez bien parcourir le numéro de téléphone qui commence par un caractère spécial ou un chiffre, plus n’importe quel nombre et n’importe quel type de séparateurs utilisés entre chaque partie du numéro de téléphone.

3. Un simple test. Cela fonctionne toujours.

4. Cela marche aussi.

Voyez comment on peut rapidement perdre le contrôle d’une expression régulière. Jetez rapidement un coup d'œil sur toutes les versions précédentes. Pourriez-vous dire la différence entre la première et la suivante ?

Puisque vous comprenez encore la réponse finale (oui c’est la dernière réponse ; et si vous avez découvert un cas où cela ne fonctionne pas, je n’en veux rien savoir), on va réécrire l’expression régulière en mode détaillé, avant que vous n’oubliiez pourquoi vous avez pris les décisions que vous avez prises.

1. >>>phonePattern = re.compile(r'''

2. # n’identifie pas le début de la chaîne, les nombres peuvent démarrer n’importe où.

3. (\d{3})     # le code indicatif est de 3 chiffres (e.g. '800')

4. \D*         # le séparateur optionnel peut être tout non-numérique

5. (\d{3})     # la ligne est de 3 chiffres (e.g. '555')

6. \D*         # un séparateur optionnel

7. (\d{4})     # le reste du numéro est 4 chiffres (e.g. '1212')

8. \D*         # un séparateur optionnel

9. (\d*)       # l’extension est optionnelle et peut être tout numérique

10. $           # la fin de la chaîne

11. ''', re.VERBOSE)

12. >>>phonePattern.search('work 1-(800) 555.1212 #1234').groups()  ①

13. ('800', '555', '1212', '1234')

14. >>>phonePattern.search('800-555-1212')                          ②

15. ('800', '555', '1212', '')

1. Mis à part qu’elle est étendue en plusieurs lignes, c’est exactement la même expression régulière, donc ce n’est pas étonnant qu’elle parcoure les mêmes entrées.

2. Une dernière vérification, oui, cela fonctionne encore. Vous avez fini.

VI-C-4 - Résumé

Ceci n’est qu’un minimum de ce que les expressions régulières peuvent faire. En d’autres termes, bien que vous pensiez les avoir complètement cernées, croyez-moi vous n’avez encore rien vu.

Maintenant vous vous êtes familiarisé avec les techniques suivantes :

- ^ reconnaît le début de la chaîne ;

- $ reconnaît la fin de la chaîne ;

- \b reconnaît les limites d’un mot ;

- \d reconnaît tout caractère numérique ;

- \D reconnaît tout caractère non numérique ;

- x? reconnaît un caractère x optionnel (autrement dit, il reconnaît x zéro ou une fois) ;

- x* reconnaît x zéro fois ou plus ;

- x+ reconnaît x une fois ou plus ;

- x{n,m} reconnaît un caractère x au moins n fois, mais pas plus que m fois ;

- (a|b|c) reconnaît exactement l’une des trois possibilités a, b ou c ;

- (x) en général c’est un groupe mémorisé. Vous pouvez avoir la valeur de ce qui a été identifié en utilisant 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 bonne solution pour chaque problème. Vous devez en apprendre suffisamment sur elles pour savoir quand elles sont appropriées, quand elles résoudront vos problèmes et quand elles en causeront plus qu'elles n'en résolvent.

VII - Chapitre 6 : closures et générateurs

« My spelling is Wobbly. It’s good spelling but it Wobbles, and the letters get in the wrong places. » — Winnie-the-Pooh

VII-A - En plongée

Ayant grandi en fils de bibliothécaire et de spécialiste en langue anglaise, j'ai toujours été fasciné par les langages. Pas les langages de programmation. Enfin si, les langages de programmation, mais aussi les langages naturels. Prenez l'anglais. L'anglais est un langage schizophrénique qui emprunte des mots de l'allemand, du français, de l’espagnol et du latin (pour en citer quelques-uns). En fait, « emprunte » n'est pas le bon mot ; « pille » est plus approprié. Ou peut-être, « assimile » — comme les Borgs. Ouais, j'aime ça.

Nous sommes les Borgs. Vos caractéristiques linguistiques et étymologiques seront ajoutées aux nôtres. La résistance est futile.

Dans ce chapitre, vous allez apprendre les noms pluriels. Mais aussi, les fonctions qui retournent d'autres fonctions, les expressions régulières avancées et les générateurs. Mais d'abord, discutons de comment former les noms pluriels. (Si vous n'avez pas encore lu le chapitre sur les expressions régulières, maintenant serait le bon moment. Ce chapitre suppose que vous comprenez les bases des expressions régulières et il en vient rapidement à des usages plus avancés.)

Si vous avez grandi dans un pays anglophone ou appris l'anglais dans un contexte scolaire, vous êtes probablement familier avec les règles de base :

- si un mot se termine avec un S, X, ou Z, ajoutez ES. Bass devient basses, fax devient faxes et waltz devient waltzes ;

- si un mot se termine avec un H sonore, ajoutez ES ; s'il se termine avec un H muet, ajoutez juste un S. Qu'est-ce qu'un H sonore ? C'est un H qui, combiné avec d'autres lettres, forme un son que vous pouvez entendre. Donc coach devient coaches et rash devient rashes, parce que vous pouvez entendre les sons CH et SH quand vous les prononcez. Mais cheetah devient cheetahs, parce que le H est muet ;

- si un mot se termine avec un Y qui sonne comme in I, remplacez le Y par IES ; si le Y est combiné avec une voyelle pour sonner différemment, ajoutez juste un S. Donc vacancy devient vacancies, mais day devient days ;

- si tout échoue, alors ajoutez simplement S et espérez que ça ira.

Je sais, il y a plein d'exceptions. Man devient men et woman devient women, mais human devient humans. Mouse devient mice et louse devient lice, mais house devient houses. Knife devient knives et wife devient wives, mais lowlife devient lowlifes. Et ne parlons même pas des mots qui sont leur propre pluriel comme sheep, deer et haiku.

Bien évidemment, les autres langues sont complètement différentes.

Développons une librairie Python qui forme automatiquement le pluriel des mots anglais. Nous commencerons seulement avec ces quatre règles, mais gardez à l'esprit que vous allez inévitablement en ajouter d'autres.

VII-B - Je sais, utilisons les expressions régulières !

Donc, vous recherchez des mots qui, au moins en anglais, signifient que vous recherchez des chaînes de caractères. Vous avez des règles qui disent que vous devez trouver différentes combinaisons de caractères et leur faire subir différentes choses. Cela semble être un boulot pour les expressions régulières !

1. import re

3. def plural(noun):

4. if re.search('[sxz]$', noun):             ### (1)

5. return re.sub('$', 'es', noun)        ### (2)

6. elif re.search('[^aeioudgkprt]h$', noun):

7. return re.sub('$', 'es', noun)

8. elif re.search('[^aeiou]y$', noun):

9. return re.sub('y$', 'ies', noun)

10. else:

11. return noun + 's'

(1) C'est une expression régulière, mais elle utilise une syntaxe que vous n'avez pas vu dans le chapitre Expressions Régulières. Les crochets signifient « correspond exactement à l’un de ces caractères ». Donc [sxz] signifie «  s, ou x, ou z », mais seulement à l’un d'entre eux. Le $ devrait être familier ; il correspond à la fin de chaîne. Cette expression régulière vérifie donc si noun se termine avec s, x ou z.

(2) Cette fonction re.sub() exécute une substitution de chaînes par expression régulière.

Regardons les substitutions par expression régulière plus en détail.

1. >>> import re

2. >>> re.search('[abc]', 'Mark')    ### (1)

3. <_sre.SRE_Match object at 0x001C1FA8>

4. >>> re.sub('[abc]', 'o', 'Mark')  ### (2)

5. 'Mork'

6. >>> re.sub('[abc]', 'o', 'rock')  ### (3)

7. 'rook'

8. >>> re.sub('[abc]', 'o', 'caps')  ### (4)

9. 'oops'

(1) Est-ce que la chaîne Mark contient a, b, ou c ? Oui, elle contient a.

(2) OK, maintenant trouvons a, b, ou c, et remplaçons-les par o. Mark devient Mork.

(3) La même fonction transforme rock en rook.

(4) Vous devez penser que cela devrait transformer caps en oaps, mais en fait non. re.sub remplace toutes les correspondances, pas seulement la première. Donc cette expression régulière transforme caps en oops, parce que le c et le a sont tous les deux transformés en o.

Et maintenant, revenons à la fonction plural()…

1. def plural(noun):

2. if re.search('[sxz]$', noun):

3. return re.sub('$', 'es', noun)         ### (1)

4. elif re.search('[^aeioudgkprt]h$', noun):  ### (2)

5. return re.sub('$', 'es', noun)

6. elif re.search('[^aeiou]y$', noun):        ### (3)

7. return re.sub('y$', 'ies', noun)

8. else:

9. return noun + 's'

(1) Ici, vous remplacez la fin de chaîne (correspondant à $) par la chaîne es. En d'autres termes, cela ajoute es à la chaîne. Vous pouvez obtenir la même chose avec la concaténation de chaîne, par exemple noun + 'es', mais j'ai choisi d'utiliser les expressions régulières pour chaque règle, pour des raisons qui deviendront plus claires plus loin dans ce chapitre.

(2) Regardez attentivement, c'est une autre nouvelle variation. Le ^, en premier caractère entre crochets, signifie quelque chose de spécial : la négation. [^abc] signifie « n'importe quel caractère sauf a, b, ou c », donc [^aeioudgkprt] signifie n'importe quel caractère sauf a, e, i, o, u, d, g, k, p, r, et t. Puis ce caractère doit être suivi par h, suivi par la fin de chaîne. Vous cherchez les mots qui finissent avec un H où le H est sonore.

(3) Même motif ici : trouver les mots qui se terminent par Y, où le caractère avant le Y n'est pas a, e, i, o, ni u. Vous cherchez les mots qui finissent avec un Y qui sonne comme un I.

Regardons la négation d'expression régulière plus en détail.

1. >>> import re

2. >>> re.search('[^aeiou]y$', 'vacancy')  ### (1)

3. <_sre.SRE_Match object at 0x001C1FA8>

4. >>> re.search('[^aeiou]y$', 'boy')      ### (2)

5. >>>

6. >>> re.search('[^aeiou]y$', 'day')

7. >>>

8. >>> re.search('[^aeiou]y$', 'pita')     ### (3)

9. >>>

(1) vacancy correspond à cette expression régulière, parce qu'il se termine en cy, et c n'est pas a, e, i, o, ou u.

(2) boy ne correspond pas, parce qu'il se termine en oy, et vous avez spécifiquement dit que le caractère avant le y ne peut pas être o. day ne correspond pas, parce qu'il se termine en ay.

(3) pita ne correspond pas, parce qu'il ne se termine pas en y.

1. >>> re.sub('y$', 'ies', 'vacancy')               ### (1)

2. 'vacancies'

3. >>> re.sub('y$', 'ies', 'agency')

4. 'agencies'

5. >>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')  ### (2)

6. 'vacancies'

(1) Cette expression régulière transforme vacancy en vacancies et agency en agencies, ce qui est ce que vous voulez. Notez que cela transforme aussi boy en boies, mais cela n'arrivera jamais dans la fonction parce que vous avez fait cette recherche d'abord avec re.search pour savoir si vous deviez la faire avec re.sub.

(2) Juste en passant, je voudrais mettre en évidence le fait qu'il est possible de combiner ces deux expressions régulières (l’une pour trouver si la règle s'applique et l’autre pour effectivement l'appliquer) en une unique expression régulière. Cela ressemblerait à ça. Cela doit sembler en grande partie familier : vous utilisez un groupe de capture, que vous avez appris dans l’ étude de cas  «  analyser les numéros de téléphone ». Le groupe est utilisé pour capturer le caractère avant la lettre y. Puis dans la chaîne de substitution, vous utilisez une nouvelle syntaxe, \1, qui signifie « hé, tu te souviens de ce premier groupe ? Mets-le ici. » Dans ce cas, vous capturez le c avant le y ; quand vous faites la substitution, vous remplacez c par c et y par ies. (Si vous avez plus d'un groupe de capture, vous pouvez utiliser \2 et \3 et ainsi de suite.).

Les substitutions par expression régulière sont extrêmement puissantes et la syntaxe \1 les rend encore plus puissantes. Mais combiner toute l’opération en une seule expression régulière est aussi plus difficile à lire et cela ne correspond pas directement à la façon dont vous décrivez les règles du pluriel. Vous avez initialement posé les règles comme « si le mot se termine en S, X, ou Z, alors ajouter ES ». Si vous regardez cette fonction, vous avez deux lignes de code qui disent « si le mot se termine en S, X, ou Z, alors ajouter ES ». Il n'est pas possible d'être plus direct.

VII-C - Une liste de fonctions

Maintenant, vous allez ajouter un niveau d'abstraction. Vous avez commencé par définir une liste de règles : si ceci, faire cela, autrement aller à la prochaine règle. Compliquons temporairement une partie du programme, ainsi vous pourrez en simplifier une autre partie.

1. import re

3. def match_sxz(noun):

4. return re.search('[sxz]$', noun)

6. def apply_sxz(noun):

7. return re.sub('$', 'es', noun)

9. def match_h(noun):

10. return re.search('[^aeioudgkprt]h$', noun)

12. def apply_h(noun):

13. return re.sub('$', 'es', noun)

15. def match_y(noun):                             ### (1)

16. return re.search('[^aeiou]y$', noun)

18. def apply_y(noun):                             ### (2)

19. return re.sub('y$', 'ies', noun)

21. def match_default(noun):

22. return True

24. def apply_default(noun):

25. return noun + 's'

27. rules = ((match_sxz, apply_sxz),               ### (3)

28. (match_h, apply_h),

29. (match_y, apply_y),

30. (match_default, apply_default)

31. )

33. def plural(noun):

34. for matches_rule, apply_rule in rules:     ### (4)

35. if matches_rule(noun):

36. return apply_rule(noun)

(1) Maintenant, chaque règle de correspondance est sa propre fonction qui retourne les résultats de l'appel de la fonction re.search().

(2) Chaque règle d'application est aussi sa propre fonction qui appelle la fonction re.sub() pour appliquer la règle de pluriel appropriée.

(3) Au lieu d'avoir une fonction (plural()) avec de multiples règles, vous avez la structure de données rules qui est une séquence de paires de fonctions.

(4) Puisque les règles ont été placées dans une structure de données séparée, la nouvelle fonction plural() peut être réduite à quelques lignes de code. En utilisant une boucle for, vous pouvez extraire les règles de correspondance et les règles d'application deux à la fois (une de correspondance et une d'application) de la structure rules. À la première itération de la boucle for, matches_rule recevra match_sxz, et apply_rule recevra apply_sxz. À la seconde itération (en présumant que vous arrivez jusque là), matches_rules sera assignée à match_h, et apply_rule sera assignée à apply_h. La fonction retourne systématiquement quelque chose, éventuellement parce que la dernière règle (match_default) retourne simplement True, cela signifie que la règle d'application correspondante (apply_default) sera toujours appliquée.

La raison pour laquelle cette technique fonctionne est que tout en Python est un objet, y compris les fonctions. La structure de données rules contient des fonctions — pas des noms de fonctions, mais de vrais objets fonction. Quand ils sont assignés dans la boucle for, alors matches_rule et apply_rule sont de vraies fonctions que vous pouvez appeler. À la première itération de la boucle for, c'est équivalent à un appel de matches_sxz(noun), et si elle retourne une correspondance, cela revient à appeler apply_sxz(noun).

Si ce niveau d'abstraction additionnel prête à confusion, essayez de dérouler la fonction pour voir l’équivalence. La boucle for complète est équivalente à :

1. def plural(noun):

2. if match_sxz(noun):

3. return apply_sxz(noun)

4. if match_h(noun):

5. return apply_h(noun)

6. if match_y(noun):

7. return apply_y(noun)

8. if match_default(noun):

9. return apply_default(noun)

Le bénéfice ici est que la fonction plural() est maintenant simplifiée. Elle prend une séquence de règles, définies quelque part et les itère d'une manière générique.

1. Trouver une règle de correspondance.

2. Une correspondance ? Alors, appeler la règle d'application et retourner le résultat.

3. Pas de correspondance ? Aller a l’étape 1.

Les règles peuvent être définies n'importe où, de n'importe quelle façon. La fonction plural() ne s'en préoccupe pas.

Maintenant, est-ce qu'ajouter ce niveau d'abstraction en valait la peine ? Et bien, pas encore. Considérez ce qu'il faudrait pour ajouter une nouvelle règle à la fonction. Dans le premier exemple, cela nécessiterait d'ajouter une instruction if à la fonction plural(). Dans le second exemple, cela nécessiterait d'ajouter deux fonctions, match_foo() et apply_foo(), puis de mettre à jour la séquence rules pour spécifier où dans la séquence, les nouvelles fonctions de correspondance et d'application doivent être appelées par rapport aux autres règles.

Mais ce n'est qu'une étape dans la prochaine section. Allons-y…

VII-D - Une liste de motifs

Définir des fonctions nommées, séparées pour chaque règle de correspondance et d'application, n'est pas vraiment nécessaire. Vous ne les appelez jamais directement ; vous les ajoutez à la séquence rules et les appelez depuis-là. De plus, chaque fonction suit l’un des deux motifs. Toutes les fonctions de correspondance appellent re.search() et toutes les fonctions d'applications appellent re.sub(). Examinons les motifs afin de définir de nouvelles règles plus faciles :

1. import re

3. def build_match_and_apply_functions(pattern, search, replace):

4. def matches_rule(word):                                     ### (1)

5. return re.search(pattern, word)

6. def apply_rule(word):                                       ### (2)

7. return re.sub(search, replace, word)

8. return (matches_rule, apply_rule)                           ### (3)

(1) build_match_and_apply_functions() est une fonction qui construit d'autres fonctions de manière dynamique. Elle prend pattern, search et replace, puis définit une fonction matches_rule() qui appelle re.search() avec le pattern (motif) qui a été passé à la fonction build_match_and_apply_functions() et le word (mot) qui a été passé à la fonction matches_rule() que vous construisez. Waouh.

(2) Construire la fonction d'application fonctionne de la même manière. La fonction d'application est une fonction qui prend un paramètre et qui appelle re.sub() avec les paramètres search et replace. Ces paramètres search et replace ont été passés à la fonction build_match_and_apply_functions() et (le mot) word a été passé à la fonction apply_rule() que vous construisez. Cette technique utilisant les valeurs de paramètres externes au sein d'une fonction dynamique est appelée closures. Vous définissez essentiellement des constantes au sein de la fonction d'application que vous construisez : elle prend un paramètre (word), mais elle le traite alors avec deux autres valeurs (search et replace) qui ont été paramétrées quand vous avez défini la fonction application.

(3) Finalement, la fonction build_match_and_apply_functions() retourne un tuple de deux valeurs : les deux fonctions que vous venez de créer. Les constantes que vous avez définies dans ces fonctions (pattern dans la fonction matches_rule() et search dans la fonction apply_rule()) restent avec ces fonctions, même après votre retour de build_match_and_apply_functions(). C'est incroyablement pratique.

Si cela prête vraiment à confusion, cela devrait devenir plus clair en regardant comment l'utiliser.

1. patterns = \                                                        ### (1)

2. (

3. ('[sxz]$',           '$',  'es'),

4. ('[^aeioudgkprt]h$', '$',  'es'),

5. ('(qu|[^aeiou])y$',  'y$', 'ies'),

6. ('$',                '$',  's')                                 ### (2)

7. )

8. rules = [build_match_and_apply_functions(pattern, search, replace)  ### (3)

9. for (pattern, search, replace) in patterns]

(1) Nos « règles » du pluriel sont maintenant définies comme tuples de tuples de chaînes (non pas de fonctions). La première chaîne dans chaque groupe est le motif d'expression régulière que vous utiliseriez avec re.search() pour voir si la règle correspond. La deuxième et la troisième chaîne dans chaque groupe sont les expressions de recherche et de remplacement que vous utiliseriez avec re.sub() pour effectivement appliquer la règle afin de transformer un nom en son pluriel.

(2) Il y a un léger changement ici dans la règle par défaut. Dans l'exemple précédent, la fonction match_default() retournait simplement True, signifiant que si aucune des règles spécifiques ne correspondait, le code ajouterait simplement un s à la fin du mot donné. Cet exemple fait quelque chose de fonctionnellement équivalent. La dernière expression régulière demande si le mot a une fin ($ correspond à la fin d'une chaîne). Bien sûr, toute chaîne a une fin, même une chaîne vide, donc cette expression correspond toujours. Ainsi, elle sert le même objectif que la fonction matches_default() qui retournait toujours True : elle assure que si aucune règle spécifique ne correspond, le code ajoute un s à la fin du mot donné.

(3) Cette ligne est magique. Elle prend la séquence de chaînes dans patterns et la transforme en une séquence de fonctions. Comment ? En faisant correspondre les chaînes à la fonction build_match_and_apply_functions(). C'est-à-dire qu’elle prend chaque triplet de chaînes et appelle la fonction build_match_and_apply_functions() avec ces trois chaînes comme arguments. La fonction build_match_and_apply_functions() retourne un tuple de deux fonctions. Cela signifie que rules est donc fonctionnellement équivalente à l'exemple précédent : une liste de tuples où chaque tuple est une paire de fonctions. La première est la fonction de correspondance qui appelle re.search() et la seconde est la fonction d'application qui appelle re.sub().

Cette version du script est complétée par le point d’entrée principal, la fonction plural().

1. def plural(noun):

2. for matches_rule, apply_rule in rules:  ### (1)

3. if matches_rule(noun):

4. return apply_rule(noun)

(1) Puisque la liste rules est la même que dans l'exemple précédent (c'est vraiment le cas), cela ne devrait pas être surprenant que la fonction plural() n'ait pas changé du tout. Elle est complètement générique ; elle prend une liste de fonctions de règle et appelle chacune dans l'ordre. Elle ne se soucie pas de comment les règles sont définies. Dans l'exemple précédent, elles étaient définies comme des fonctions nommées séparées. Maintenant, elles sont construites dynamiquement en faisant correspondre la sortie de la fonction build_match_and_apply_functions() avec une liste brute de chaînes. Cela n'a aucune importance ; la fonction plural() fonctionne toujours de la même façon.

VII-E - Un fichier de motifs

Vous avez extrait tout le code dupliqué et ajouté assez d'abstraction pour que les règles du pluriel soient définies dans une liste de chaînes. La prochaine étape logique est de prendre ces chaînes et de les mettre dans un fichier spécifique, où elles peuvent être maintenues séparément du code qui les utilise.

En premier, créons un fichier texte qui contient les règles que vous voulez. Pas de structure de données sophistiquée, juste des chaînes délimitées par des espaces dans trois colonnes. Appelons-le plural4-rules.txt.

1. [sxz]$               $    es

2. [^aeioudgkprt]h$     $    es

3. [^aeiou]y$          y$    ies

4. $                    $    s

Maintenant, regardons comment nous pouvons utiliser ce fichier de règles.

1. import re

3. def build_match_and_apply_functions(pattern, search, replace):     ### (1)

4. def matches_rule(word):

5. return re.search(pattern, word)

6. def apply_rule(word):

7. return re.sub(search, replace, word)

8. return (matches_rule, apply_rule)

10. rules = []

11. with open('plural4-rules.txt', encoding='utf-8') as pattern_file:  ### (2)

12. for line in pattern_file:                                      ### (3)

13. pattern, search, replace = line.split(None, 3)             ### (4)

14. rules.append(build_match_and_apply_functions(              ### (5)

15. pattern, search, replace))

(1) La fonction build_match_and_apply_functions() n'a pas changé. Vous utilisez toujours des closures pour construire deux fonctions dynamiquement qui utilisent les variables définies dans la fonction externe.

(2) La fonction globale open() ouvre un fichier et retourne un objet fichier. Dans ce cas, le fichier que nous ouvrons contient les motifs de chaînes pour mettre au pluriel des noms. L'instruction with crée ce qui est appelé un contexte : quand le bloc with se termine, Python va automatiquement fermer le fichier, même si une exception se produit à l’intérieur du bloc with. Vous en apprendrez plus à-propos des blocs with et des objets fichier dans le chapitre Fichiers.

(3) L’expression for line in <fileobject> lit les données depuis le fichier ouvert, une ligne à la fois et assigne le texte à la variable line. Vous en apprendrez plus à-propos de la lecture de fichiers dans le chapitre Fichiers.

(4) Chaque ligne du fichier a en fait trois valeurs, mais elles sont séparées par des espaces blancs (tabulation ou espace, cela n'a pas d'importance). Pour les séparer, utilisez la méthode de chaîne split(). Le premier argument de la méthode split() est None, ce qui signifie « découpe à chaque espace blanc (tabulation ou espace, cela n'a pas d'importance). » Le second argument est 3, ce qui signifie « découpe sur les espaces blancs 3 fois, puis laisse en l’état le reste de la ligne ». Une ligne comme [sxz]$ $ es sera découpée en liste ['[sxz]$', '$', 'es'], ce qui signifie que pattern aura pour valeur '[sxz]$', search aura '$' et replace aura 'es'. C'est beaucoup de puissance dans une seule petite ligne de code.

(5) Finalement, vous passez pattern, search, et replace à la fonction build_match_and_apply_functions(), qui retourne un tuple de fonctions. Vous ajoutez ce tuple à la liste rules, et rules finit par stocker la liste des fonctions de correspondance et d'application que la fonction plural() attend.

L’amélioration ici est que vous avez complètement séparé les règles du pluriel dans un fichier externe, ainsi, il peut être maintenu séparément du code qui l'utilise. Le code avec le code, les données avec les données et tout va bien.

VII-F - Générateurs

Est-ce que ce ne serait pas grandiose d'avoir une fonction générique plural() qui analyse le fichier de règles, récupère les règles, cherche une correspondance, applique la transformation appropriée, va à la règle suivante. C'est tout ce que la fonction plural() a à faire et c'est tout ce que la fonction plural() devrait faire.

1. def rules(rules_filename):

2. with open(rules_filename, encoding='utf-8') as pattern_file:

3. for line in pattern_file:

4. pattern, search, replace = line.split(None, 3)

5. yield build_match_and_apply_functions(pattern, search, replace)

7. def plural(noun, rules_filename='plural5-rules.txt'):

8. for matches_rule, apply_rule in rules(rules_filename):

9. if matches_rule(noun):

10. return apply_rule(noun)

11. raise ValueError('no matching rule for {0}'.format(noun))

Mais comment est-ce que ça fonctionne ? Regardons d'abord un premier exemple interactif.

1. >>> def make_counter(x):

2. …     print('entering make_counter')

3. …     while True:

4. …         yield x                    ### (1)

5. …         print('incrementing x')

6. …         x = x + 1

7. …

8. >>> counter = make_counter(2)          ### (2)

9. >>> counter                            ### (3)

10. <generator object at 0x001C9C10>

11. >>> next(counter)                      ### (4)

12. entering make_counter

13. 2

14. >>> next(counter)                      ### (5)

15. incrementing x

16. 3

17. >>> next(counter)                      ### (6)

18. incrementing x

19. 4

(1) La présence du mot-clef yield dans make_counter signifie que ce n'est pas une fonction normale. C'est un type spécial de fonction qui génère des valeurs l'une après l'autre. Vous pouvez l'imaginer comme une fonction pouvant être interrompue et reprise. L'appeler retournera un générateur qui peut être utilisé pour générer des valeurs successives de x.

(2) Pour créer une instance du générateur make_counter, appelez-le juste comme une autre fonction. Notez qu'en fait cela n’exécute pas le code de la fonction. Vous pouvez le voir parce que la première ligne de la fonction make_counter appelle print(), mais rien n'est encore affiché.

(3) La fonction make_counter retourne un objet générateur.

(4) La fonction next() prend un objet générateur et retourne sa prochaine valeur. La première fois que vous appelez next() avec le générateur counter, elle exécute le code dans make_counter jusqu'à la première instruction yield, puis retourne la valeur qui a été générée. Dans ce cas, ce sera 2, car vous avez initialement créé le générateur en appelant make_counter(2).

(5) Appeler de façon répétitive next() avec le même objet générateur reprend le traitement exactement là où il s'était arrêté et continue jusqu'à ce qu'il rencontre la prochaine instruction yield. Toutes les variables, état local, etc., sont sauvegardées à yield et rétablies à next(). La prochaine ligne de code attendant d’être exécutée appelle print(), qui affiche incrementing x. Après cela, l'instruction x = x + 1 est exécutée. Puis elle boucle grâce à l’instruction while et la première chose rencontrée est l'instruction yield x, qui sauvegarde l’état de toute chose et retourne la valeur courante de x (maintenant 3).

(6) La deuxième fois que vous appelez next(), vous faites à nouveau les mêmes choses, mais cette fois x vaut 4.

Puisque make_counter est une boucle infinie, vous pouvez théoriquement faire cela indéfiniment et ça continuerait juste à incrémenter x et restituer des valeurs. Mais regardons plutôt des utilisations plus productives des générateurs.

“yield” met en pause une fonction. “next()” la reprend où elle s’était interrompue.

VII-F-1 - Un générateur Fibonacci

1. def fib(max):

2. a, b = 0, 1          ### (1)

3. while a < max:

4. yield a          ### (2)

5. a, b = b, a + b  ### (3)

(1) La séquence de Fibonacci est une séquence de nombres où chaque nombre est la somme des deux nombres qui le précèdent. Elle commence avec 0 et 1, progresse doucement d'abord, puis de plus en plus rapidement. Pour commencer la séquence, vous avez besoin de deux variables : a commence à 0, et b commence à 1.

(2) Générons a qui est le nombre courant dans la séquence.

(3) Assignons b, qui est le prochain nombre dans la séquence, à a, mais calculons aussi la prochaine valeur (a + b) et assignons-la à b pour l'utiliser plus tard. Notez que cela se produit en parallèle ; si a vaut 3 et b vaut 5, alors a, b = b, a + b paramétrera 5 à a (la précédente valeur de b) et 8 à b (la somme des précédentes valeurs de a et b).

Donc, vous avez une fonction qui restitue les nombres Fibonacci successifs. Bien sûr, vous pouvez faire cela avec la récursion, mais cette manière est plus facile à lire. De même, cela fonctionne bien avec les boucles for.

1. >>> from fibonacci import fib

2. >>> for n in fib(1000):      ### (1)

3. …     print(n, end=' ')    ### (2)

4. 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

5. >>> list(fib(1000))          ### (3)

6. [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

(1) Vous pouvez utiliser un générateur tel que fib() directement dans la boucle for. La boucle for appellera automatiquement la fonction next(), récupérera les valeurs du générateur fib() et les assignera à la variable d'index (n) de la boucle for.

(2) A chaque passage dans la boucle for, n reçoit une nouvelle valeur de l'instruction yield dans fib(), tout ce que vous avez à faire est de l'afficher. Une fois que fib() est à court de nombres (a devient plus grand que max, qui dans ce cas vaut 1000), alors la boucle for se termine normalement.

(3) C'est un idiome utile : passez un générateur à la fonction list(), il itérera le générateur tout entier (exactement comme la boucle for de l'exemple précédent) et retournera une liste de toutes les valeurs.

VII-F-2 - Un générateur de règles du pluriel

Retournons à plural5.py et voyons comment cette version de plural() fonctionne.

1. def rules(rules_filename):

2. with open(rules_filename, encoding='utf-8') as pattern_file:

3. for line in pattern_file:

4. pattern, search, replace = line.split(None, 3)                   ### (1)

5. yield build_match_and_apply_functions(pattern, search, replace)  ### (2)

7. def plural(noun, rules_filename='plural5-rules.txt'):

8. for matches_rule, apply_rule in rules(rules_filename):                   ### (3)

9. if matches_rule(noun):

10. return apply_rule(noun)

11. raise ValueError('no matching rule for {0}'.format(noun))

(1) Ici, pas de magie. Souvenez-vous que les lignes du fichier de règles ont trois valeurs séparées par des espaces, donc vous utilisez line.split(None, 3) pour récupérer les trois « colonnes » et les assigner à trois variables locales.

(2) Et puis vous générez. Qu'est-ce que vous générez ? Deux fonctions, construites dynamiquement avec notre vieille amie, build_match_and_apply_functions(), qui est identique à celle des exemples précédents. En d'autres termes, rules() est un générateur qui restitue des fonctions de correspondance et d'application à la demande.

(3) Puisque rules() est un générateur, vous pouvez l'utiliser directement dans une boucle for. Au premier passage dans la boucle for, vous appellerez la fonction rules(), qui ouvrira le fichier de motifs, lira la première ligne, construira dynamiquement une fonction de correspondance et une fonction d'application à partir des motifs de cette ligne, puis générera les fonctions construites dynamiquement. Au second passage dans la boucle for, vous reprendrez exactement là où vous avez laissé rules() (qui était au milieu de la boucle for line in pattern_file). La première chose qu'elle fera, sera de lire la ligne suivante du fichier (qui est toujours ouvert). Ensuite, une autre fonction de correspondance et une autre fonction d'application sur la base des motifs de cette ligne seront construites dynamiquement puis générées.

Qu'avez-vous gagné par rapport à l’étape 4 ? Le temps de démarrage. À l’étape 4, quand vous importez le module plural4, il lit tout le fichier de motifs et construit une liste de toutes les règles possibles, avant même que ne vous pensiez à appeler la fonction plural(). Avec les générateurs, vous pouvez tout faire de façon paresseuse : vous lisez la première règle et créez les fonctions puis les testez, et si ça fonctionne, vous ne lisez même pas le reste du fichier et ne créez pas non plus d'autres fonctions.

Qu'avez-vous perdu ? De la performance ! Chaque fois que vous appelez la fonction plural(), le générateur rules recommence depuis le début — ce qui signifie ouvrir à nouveau le fichier de motifs et le lire depuis le début, une ligne à la fois.

Et si vous pouviez avoir le meilleur des deux mondes : temps de démarrage minimal (ne pas exécuter de code à import) et performance maximale (ne pas construire les mêmes fonctions encore et encore). Oh et vous voulez toujours garder les règles dans un fichier séparé (parce que le code avec le code et les données avec les données), tant que vous n'avez jamais besoin de lire deux fois la même ligne.

Pour faire cela, vous allez devoir construire votre propre itérateur. Mais avant que vous fassiez cela, vous devez apprendre les classes Python.

VII-G - Pour aller plus loin (en anglais)

- PEP 255: Simple Generators

- Understanding Python’s “with” statement

- Closures in Python

- Fibonacci numbers

(1)N.D.T. : plain texte en anglais dans le document original

(2) N.D.T. : dans le standard Unicode, un plan est un groupe continu de 65536 nombres servant à encoder

(3) Gros-boutiste : appelé aussi «gros boutien», signifie que l’octet le plus fort est enregistré ou transmis en premier et l’octet de poids le plus faible, en suivant.

(4) Petit-boutiste : appelé aussi «petit boutien», signifie que l’octet le plus faible est enregistré ou transmis en premier et l’octet de poids le plus fort, en suivant.

(5)N.D.T. : une docstring est littéralement une chaîne de caractères littérale de documentation.

(6) N.D.T. : littéralement fouet de papaye, nom d'une couleur dont le code hexadécimal est #FFEFD5

(7)(N.D.T : float en anglais)

(8)Le caractère esperluette est le symbole &

(9)N.D.T. : bytes signifie octets