Python, de zéro

Frédéric Lang

Cet article est un cours qui se veut complet sur l'apprentissage de la programmation Python.

Il présentera comment faire ses premiers pas en Python et comprendre sa syntaxe, comment créer (et exécuter) ses premiers scripts, son typage allant du plus simple au plus complexe.

Il détaillera les chaînes, leur encodage et les différentes façons de les afficher. Il décrira aussi les fonctions et leurs paramètres dans différentes configurations, les itérables, les générateurs et la syntaxe des fameuses listes de compréhension. Puis il abordera l'objet en Python avec les classes et les métaclasses et enfin le gestionnaire de contexte et les décorateurs.

Et il terminera avec quelques petits exemples variés.

Il se veut complet, mais Python est vaste, donc toutes les notions présentées seront expliquées le plus clairement possible et généralement accompagnées d'un exemple ou deux, mais sans s'y attarder plus que cela, laissant le lecteur désireux de les approfondir devoir chercher ailleurs les détails ou exercices qui lui manqueront, comme sur cette page dédiée à différents exercices Python

Commentez

- Titre : Python, de zéro

- Auteur : Frédéric Lang

- Parution : 18 septembre 2022

- Licence : Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.

Sommaire

- Préface

- Licence de ce document

- Registre des éditions

- I. Introduction

- I-1. Pourquoi programmer en Python ?

- I-2. Caractéristiques principales

- I-3. Python 2 ou Python 3 ?

- II. Premiers pas

- II-1. Invoquer l’interpréteur

- II-2. Mode interactif

- II-3. Introduction aux fonctions

- II-4. Mode script

- II-5. Les commentaires

- II-6. La fonction « print »

- II-7. le point‑virgule

- II-8. l’exception

- III. Caractéristiques du langage

- III-1. la tabulation

- III-2. la ponctuation

- IV. Introduction informelle à Python

- IV-1. Les variables

- IV-1-a. Préambule

- IV-1-b. Le typage

- IV-1-c. Les annotations

- IV-2. Les opérateurs

- IV-2-a. Arithmétiques et logiques

- IV-2-b. Relationnels

- IV-2-c. Connecteurs

- IV-2-d. Priorités

- IV-3. Les chaînes de caractères

- IV-4. Les tuples

- IV-5. Les listes

- IV-6. Les ensembles

- IV-7. Les dictionnaires

- IV-8. True, False, None

- IV-9. Les mots clefs

- IV-10. Tout est objet

- IV-11. La mutabilité

- V. Les débuts de la programmation

- V-1. L’instruction « if »

- V-2. L’instruction « while »

- V-3. L’instruction « for »

- V-4. Les instructions « break » et « continue », et les clauses « else » au sein des boucles

- V-5. L’instruction « pass »

- V-6. Les instructions « do…while » et « switch…case »

- V-7. L’instruction « match »

- V-8. La notion d’expression

- V-9. Quelques outils

- VI. la saisie clavier

- VI-1. Python 2

- VI-2. Python 3

- VII. Formatage des chaînes

- VII-1. Le formatage originel

- VII-2. La méthode « format() »

- VII-2-a. Pour formater une chaîne

- VII-2-b. Pour formater un entier

- VII-2-c. Pour formater un flottant

- VII-2-d. Remarque

- VII-3. La méthode « f-strings »

- VII-3-a. Pour formater une chaîne de caractères

- VII-3-b. Pour formater un entier

- VII-3-c. Pour formater un nombre à virgule flottante

- VII-3-d. Remarque

- VIII. L’encodage d’une chaîne

- VIII-1. La chaîne de base

- VIII-2. Le préfixe « u »

- VIII-3. Le préfixe « b »

- VIII-4. Le préfixe « r »

- VIII-5. Récapitulatifs

- IX. Les fonctions

- IX-1. Généralités

- IX-2. Valeur par défaut des paramètres

- IX-3. Les paramètres nommés

- IX-4. Verrouillage des méthodes de passage des arguments

- IX-5. Les annotations

- IX-6. Les paramètres supplémentaires facultatifs

- IX-7. Utilisation inverse

- IX-8. Détails de syntaxe

- IX-9. Fonctions anonymes

- IX-10. Les fonctions incluses

- IX-11. La récursivité

- IX-11-a. Le concept

- IX-11-b. Les inconvénients

- IX-11-c. Optimisation

- X. Les iterables

- X-1. La notion d’« itérable »

- X-2. Les outils Python

- XI. Les générateurs

- XI-1. Introduction

- XI-2. Exemple

- XI-3. Différentes actions

- XI-4. Quelques précautions

- XI-5. Générateur d’un itérable

- XII. La portée et la visibilité des variables

- XII-1. La portée

- XII-2. La visibilité

- XII-2-a. Principes

- XII-2-b. Conséquences

- XII-3. La portée

- XII-4. Identité et hash d’une variable

- XII-4-a. Identité d’une variable

- XII-4-b. Hash d’une variable

- XII-5. De la bonne façon de copier les choses

- XIII. Les listes en intension

- XIV. Les classes

- XIV-1. Préambule

- XIV-2. L’instanciation et les instances

- XIV-3. Les attributs

- XIV-4. Les méthodes

- XIV-5. La surcharge de méthodes

- XIV-6. Les attributs et méthodes privés

- XIV-6-a. Privatisation par Python

- XIV-6-b. Accès aux attributs privés

- XIV-6-c. Privatisation par convention

- XV. Compléments sur les classes

- XV-1. L’héritage

- XV-2. L’héritage face à l’évolution

- XV-3. Les attributs et méthodes statiques

- XV-3-a. Attribut statique

- XV-3-b. Méthode statique

- XV-4. La fonction « classmethod() »

- XV-5. Gestion interne des attributs

- XV-5-a. L’attribut __dict__

- XV-5-b. La méthode __delattr__()

- XV-5-c. La méthode __getattr__()

- XV-5-d. La méthode __getattribute__()

- XV-5-e. La méthode __setattr__()

- XV-5-f. La méthode __new__()

- XV-5-g. La méthode __init_subclass__()

- XV-5-h. L’attribut __slots__

- XV-6. Trucs et astuces divers sur les classes

- XV-6-a. Rendre une classe « callable »

- XV-6-b. Rendre une classe « indexable »

- XV-6-c. Rendre une classe « itérable »

- XV-6-d. Rendre une classe « mesurable »

- XV-6-e. Indiquer une contenance dans une classe

- XV-6-f. Définir les opérateurs usuels

- XV-6-g. Redéfinir les casts

- XV-6-h. Rendre une classe « hashable »

- XV-6-i. Gérer la liste des attributs listés

- XV-6-j. Gérer le formatage

- XV-7. Les fonctions sont aussi des objets

- XVI. Compléments sur les tuples, listes, ensembles, dictionnaires et chaînes

- XVI-1. Méthodes spécifiques applicables aux tuples et listes

- XVI-2. Méthodes spécifiques applicables aux ensembles

- XVI-3. Méthodes spécifiques applicables aux dictionnaires

- XVI-3-a. Pour les deux versions de Python

- XVI-3-b. Spécificités Python 2

- XVI-4. Méthodes spécifiques applicables aux chaînes

- XVI-5. Fonctions externes

- XVI-6. Modifier une séquence en cours d’itération

- XVII. Les closures

- XVII-1. Introduction

- XVII-2. L’instruction « nonlocal »

- XVII-3. À quoi cela sert-il ?

- XVIII. Les métaclasses

- XVIII-1. La fonction « type() »

- XVIII-2. Créer sa propre fonction génératrice

- XVIII-3. Puis passer par une classe

- XVIII-4. Code complet

- XVIII-5. Pour finir

- XIX. Les erreurs et les exceptions

- XIX-1. Préambule

- XIX-2. Se prémunir

- XIX-3. Les instructions « else » et « finally »

- XIX-4. L’instruction « raise »

- XIX-5. Assertion

- XIX-6. Arbre des exceptions

- XIX-7. Définir ses propres exceptions

- XIX-8. Pardon ou permission

- XX. La programmation modulaire

- XX-1. Créer ses modules

- XX-2. Tester ses modules

- XX-3. Les membres implicites

- XX-4. Import depuis un autre dossier

- XX-4-a. Import par le nom du module

- XX-4-b. Import par le nom du dossier

- XX-5. Quelques modules utiles

- XX-5-a. Modules natifs

- XX-5-b. Modules spécifiques

- XXI. Pour tester les types

- XXII. L’accès aux fichiers

- XXII-1. L’accès au fichier

- XXII-2. Traitements sur le fichier

- XXII-3. Autres fonctions de manipulation

- XXII-4. Itération

- XXIII. Le « context manager »

- XXIII-1. Préambule

- XXIII-2. Utilisation

- XXIII-3. Création de son propre « context manager »

- XXIV. Les décorateurs

- XXIV-1. Préambule

- XXIV-2. Un premier décorateur

- XXIV-3. Un peu plus complexe : un décorateur plus universel

- XXIV-4. Le danger du décorateur

- XXIV-5. Exemple : un décorateur qui gère les appels déjà connus

- XXIV-6. Encore plus complexe : donner des arguments au décorateur

- XXIV-7. L'objet décorateur

- XXIV-8. Conclusion

- XXIV-9. Dernier détail…

- XXV. La précision des nombres décimaux

- XXVI. Divers

- XXVI-1. Python Enhancement Proposals (PEP)

- XXVI-2. Zen de Python

- XXVI-3. Les docstrings

- XXVI-4. La bonne façon de faire ses tests

- XXVI-5. Différences entre Python 2 et Python 3

- XXVII. Exemples

- XXVII-1. Suite n-bonacci

- XXVII-1-a. En récursif

- XXVII-1-b. En itératif

- XXVII-1-c. Avec une pile

- XXVII-1-d. Sous forme de générateur

- XXVII-2. Tracer le scope local

- XXVII-2-a. Quand l’objet est stocké dans une variable de la fonction

- XXVII-2-b. Quand l’objet est créé à la volée sans être récupéré par une variable

- XXVII-3. Tracer le fonctionnement des paramètres par défaut

- XXVII-4. Tracer le fonctionnement des fonctions incluses

- XXVII-5. Fonction « cmp_to_key() »

- XXVII-6. Petit benchmark sur les différentes façon de copier une liste

- XXVII-7. Petit benchmark sur le append() face aux listes en compréhension

- XXVII-8. Benchmark sur le map() face aux listes en compréhension

- XXVII-8-a. Liste unique

- XXVII-8-b. Deux listes

- XXVII-8-c. Trois listes

- XXVII-8-d. Conclusion

- Remerciements

- Index

Préface

Licence de ce document

Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.

En particulier, le verbe « modifier » autorise tout lecteur éventuel à apporter au document ses propres contributions ou à corriger d'éventuelles erreurs et lui permet d'ajouter son propre copyright dans la page « Registre des éditions », mais lui interdit d'enlever les copyrights déjà présents et lui interdit de modifier les termes de cette licence.

Ce document ne peut être cédé, déposé ou distribué d'une autre manière que l'autorise la « Licence de Documentation Libre GNU » et toute tentative de ce type annule automatiquement les droits d'utiliser ce document sous cette licence. De plus, tout tiers ayant reçu copie de ce document aura le droit d'utiliser ces copies et continuera à bénéficier de ce droit tant qu'il respectera pleinement les conditions de la licence.

La version actuelle de ce document est la 1.0, et sa version la plus récente est disponible en téléchargement à l'adresse http://fr.lang.free.fr/

Copyright © 2022 Svear (svear@free.fr)

Registre des éditions

Version

Date

Description des modifications

Auteur des modifications

1.0

Janvier 2021

Mise en place du registre des éditions
Corrections mineures (syntaxe, etc.)

© Svear (svear@free.fr)

I - Introduction

Python est un langage portable, dynamique, extensible, gratuit, qui permet (sans l'imposer) une approche modulaire et orientée objet de la programmation. Python est développé depuis 1989 par Guido van Rossum et par de nombreux contributeurs bénévoles. Il est récupérable gratuitement sur le site https://www.python.org.

Le nom du langage provient de l’émission de la BBC « Monty Python’s Flying Circus »(1) et n’a rien à voir avec les reptiles.

I-1 - Pourquoi programmer en Python ?

Aucun langage de programmation n'est parfait. Il n'existe même pas un langage meilleur que d'autres ; il n'y a que des langages en adéquation ou peu conseillés pour des buts particuliers. (Herbert Mayer).

Python n’est pas le meilleur langage pour quoi que ce soit. Pour travailler sur du texte, Perl est meilleur. Pour de l’intelligence artificielle, Lisp est meilleur. Pour de l’embarqué, C est meilleur. Pour de l’asynchrone, JavaScript est meilleur.

Non. Python n’est pas le meilleur, mais seulement très bon. Toutefois, il est très bon pour tout. Python est excellent pour analyser du texte, pour faire un site web (même asynchrone), de l’administration système, des interfaces graphiques, du calcul scientifique, de la 3D, des robots d’indexation, etc. Pas le meilleur, mais très bon.

Et ceci est totalement suffisant. Cela veut dire que pour commencer un projet, n’importe quel projet, on peut partir tranquille avec Python. On ne sera pas bloqué plus tard par tel ou tel besoin. On pourra s’en sortir agréablement avec Python.

Vous faites un site en JavaScript? Pour le sysadmin, il va falloir un autre langage…

Des scripts de maintenance en Perl? J’espère que vous n’aurez pas besoin d’une GUI…

Un crawler en PHP ? Pour l’analyse des données récupérées, vous allez vous amuser…

De l’extraction de données en Ruby ? Pourvu qu’il ne faille pas traiter plusieurs To…

Ce n’est pas que vous ne pourrez pas ; ils sont tous « Turing complet » après tout. Mais vous allez vraiment galérer.

Aucun langage n’est parfait, et Python n’est pas une exception. Mais dans 99% des cas, vous pouvez avoir une solution propre et efficace en Python à la problématique que vous ne savez même pas encore que vous aurez. Et le jour où vous atteindrez la limite, alors oui, vous pourrez utiliser une autre technologie pour la contourner. Mais juste pour cette partie. La plupart du temps, ce sera une partie très réduite. Et en plus, Python pourra probablement s’interfacer nativement avec le code de l’autre langage.

Il y a des tas de raisons de ne pas choisir Python bien entendu, mais probablement la meilleure raison de le choisir, c’est qu’il y a peu de chance de se planter.

I-2 - Caractéristiques principales

Détaillons un peu les principales caractéristiques de Python, plus précisément, du langage et de ses différentes implantations actuelles :

- Python est portable, non seulement sur les différentes variantes d'Unix, mais aussi sur les OS propriétaires (MacOS, BeOS, NeXTStep, MS-DOS) et les différentes variantes de Windows ;

- Python est gratuit, mais on peut l'utiliser sans restriction dans des projets commerciaux ;

- Python convient aussi bien à des scripts d'une dizaine de lignes qu'à des projets complexes de plusieurs dizaines de milliers de lignes ;

- la syntaxe de Python est très simple et, combinée à des types de données évolués (listes, dictionnaires, objets…), conduit à des programmes à la fois très compacts et très lisibles. À fonctionnalités égales, un programme Python (abondamment commenté et présenté selon les canons standards) est souvent de 3 à 5 fois plus petit qu'un programme C ou C++ ou même Java équivalents, ce qui entraine en général un temps de développement de 5 à 10 fois plus court et une facilité de maintenance largement accrue ;

- Python gère ses ressources (mémoire, descripteurs de fichiers…) sans intervention du programmeur, par un mécanisme de comptage de références proche d'un garbage collector ;

- Python est (optionnellement) multithreadé ;

- Python est orienté objet. Il supporte l'héritage multiple et la surcharge des opérateurs ;

- Python intègre un système d'exceptions qui permettent de simplifier considérablement la gestion des erreurs ;

- Python est dynamique (l'interpréteur peut évaluer des chaînes de caractères représentant des expressions ou des instructions Python), orthogonal (un petit nombre de concepts suffit à engendrer des constructions très riches), réflexif (il supporte la métaprogrammation, par exemple la capacité pour un objet de se rajouter ou de s'enlever des attributs ou des méthodes, ou même de changer de classe en cours d'exécution) et introspectif (un grand nombre d'outils de développement, comme le débogueur ou le profiler, sont implantés en Python lui-même) ;

- Python est dynamiquement typé. Tout objet manipulable par le programmeur possède un type bien défini à l'exécution, qui n'a pas besoin d'être déclaré à l'avance ;

- Python est extensible : on peut facilement l'interfacer avec des bibliothèques C existantes. On peut aussi s'en servir comme d'un langage d'extension pour des systèmes logiciels complexes ;

- la bibliothèque standard de Python et les paquetages externes donnent accès à une grande variété de services : chaînes de caractères et expressions régulières, services Unix standards (fichiers, pipes, signaux, sockets, threads…), accès Internet (web, news, ftp, cgi, html…), persistance et bases de données, interfaces graphiques ;

- Python est un langage qui continue à évoluer, soutenu par une communauté d'utilisateurs enthousiastes et responsables, dont la plupart sont des supporters du logiciel libre. Parallèlement à l'interpréteur principal, écrit en C et maintenu par le créateur du langage, un deuxième interpréteur écrit en Java est en cours de développement.

I-3 - Python 2 ou Python 3 ?

En 2007 Python a subi une évolution majeure de Python 2 vers Python 3. Les évolutions de cette version associées à un ménage majeur destiné à clarifier le code ont rendu toute compatibilité ascendante et descendante impossible à assurer, ce qui rend ces deux versions totalement incompatibles l’une avec l’autre (sauf à passer exclusivement par la partie qui est restée commune, ce qui réduit alors nettement les possibilités offertes par ces deux versions).

Il était prévu à l’origine que Python 2 soit abandonné en 2015 (soit huit ans après). Cependant, devant l’énorme masse de bibliothèques et outils écrits en Python 2 devant être portés sous Python 3 ; face à la demande massive des développeurs n’ayant pas la possibilité matérielle de faire ce travail (venant en supplément), et dont certains considéraient encore en 2015 que Python 3 manquait encore de maturité (on a même cru un moment qu'un Python 4 arriverait avant la mort de Python 2) ; la communauté a décidé de continuer à assurer une évolution et un suivi de Python 2 qui se sont terminés définitivement au 1er janvier 2020.

La question du choix entre la version 2 ou 3 de Python ne devrait donc plus se poser. Néanmoins, il est important de savoir qu’il existe toujours de nombreux programmes qui utilisent Python 2. Ce tutoriel est donc majoritairement écrit pour Python 3 mais avec des exemples généralement utilisables dans les deux versions (2 et 3) et les quelques exemples spécifiques v2 ou v3 seront marqués à cet effet.

La dernière version (par rapport à ce document) de Python 2 est la 2.7.18 (version définitive pour Python 2 sortie en avril 2020) et la dernière version de Python 3 est la 3.10.0 (sortie en octobre 2021).

II - Premiers pas

II-1 - Invoquer l’interpréteur

L’interpréteur Python (nommé « python ») est habituellement installé dans le dossier « /usr/bin » sur les machines Unix où il est disponible (quasiment toutes). En vérifiant que le dossier « /usr/bin » est bien présent dans le PATH de votre shell Unix (généralement il y est par défaut), cela vous permettra de lancer l’interpréteur Python en tapant la commande « python ».

1. python

Sous les systèmes Windows, Python n’est pas nativement présent et doit alors être installé manuellement par l’utilisateur qui désire l’utiliser. Le choix du dossier d’installation est libre, mais dans les versions Python 2, le dossier habituel « C:\Program Files » était déconseillé dans la mesure où certaines bibliothèques Python 2 n'appréciaient pas la présence d’espaces dans le chemin du dossier d’installation. Les bibliothèques Python 3 semblent aujourd’hui avoir réglé ce souci.

Par défaut, les versions de Python 3 s’installent dans « C:\Program Files\Python3X » et les versions de Python 2 s’installent dans « C:\Python2X » (X étant dans à chaque fois le n° de la release).

Tout comme pour Unix, il est nécessaire que ce dossier d’installation soit inscrit dans le PATH de Windows. Néanmoins, les dernières versions Python vous proposent de le faire pour vous à l’installation.

Sous les systèmes de type MacOs, Python est pour l’instant installé par défaut, mais d’une part c’est la version 2.7 qui est installée ; et d’autre part il ne sera plus installé par défaut dans les nouvelles moutures d’Apple. Dans cet OS, ceux qui veulent Python 3 doivent donc aussi l’installer manuellement. Toutefois, ceux qui ont l’ordinateur connecté à Internet peuvent alors taper la commande python3 afin que le système aille automatiquement le chercher et l’installer. Ensuite, tout nouvel appel à la commande python3 permettra de lancer l’interpréteur Python.

II-2 - Mode interactif

Lorsque l’interpréteur est lancé, Python est dit être en mode interactif. Dans ce mode, il attend ses instructions avec un prompt primaire composé généralement de trois signes « supérieur » (>>>) ; et pour les instructions demandant plusieurs lignes, il affiche ensuite un prompt secondaire, composé de trois points (...).

À son lancement, l’interpréteur affiche un message de bienvenue indiquant son numéro de version et une notice de copyright avant d’afficher le prompt primaire permettant de taper la première instruction Python.

1. Python 3.4.2 (default, Oct  8 2014, 10:45:20)

2. [GCC 4.9.1] on linux

3. Type "help", "copyright", "credits" or "license" for more information.

4. >>>

Exécution d’une première instruction simple avec son résultat directement affiché…

1. >>> 2 + 2

2. 4

3. >>>

… et d’une instruction multilignes. On notera l’obligation de protéger la fin de ligne par un backslash (\) pour éviter que cette fin de ligne soit comprise par l’interpréteur comme la fin de l’instruction ; ainsi que l’apparition du prompt secondaire (...).

1. >>> 2+\

2. ... 3

3. 5

4. >>>

À noter : comme on le voit dans ces exemples, les résultats des calculs et expressions sont directement affichés après exécution. C'est une caractéristique du mode interactif qui n'existe plus dans le mode script (sera vu un peu plus bas).

II-3 - Introduction aux fonctions

Une fonction est un mécanisme permettant d’encapsuler un travail spécifique dans un nom logique individuel (nom de la fonction). Offrant ainsi à l’utilisateur la possibilité de faire exécuter le travail en appelant simplement la fonction par son nom.

L'appel est activé par l'utilisation de parenthèses placées après le nom de la fonction. Et dans les parenthèses seront placés les paramètres éventuels permettant à la fonction d'adapter son action au contexte de l’appelant.

Python possède en natif toute une masse de fonctions internes directement utilisables par le programmeur. Exemple la fonction print() permettant d'afficher une valeur à l'écran…

1. >>> print(2 + 2)

2. 4

3. >>> print(2 + 3)

4. 5

5. >>> print("Hello World !!!")

6. Hello World !!!

7. >>> print("Hello\

8. ... World !!!")

9. Hello World !!!

10. >>> print()

12. >>>

On verra plus tard comment créer nos propres fonctions.

II-4 - Mode script

Le mode interactif, utile pour tester des instructions simples, montrera vite ses limites pour produire des codes complexes. Une autre façon de faire (plus classique chez les développeurs) sera de créer un fichier texte (d’extension conventionnellement définie à « .py ») pour y inscrire l’ensemble des instructions composant un programme Python.

Ce fichier sera alors considéré comme un code source Python. Une fois écrit, il suffira d'appeler l'interpréteur en lui demandant de traiter le fichier pour que Python exécute les instructions s’y trouvant.

Exemple (depuis un shell Unix) :

Lancement de l’éditeur « vi » :

1. PromptUnix> vi prog.py

Demande à l'interpréteur Python d'exécuter le script :

1. PromptUnix> python prog.py

2. Hello World !!!

Contenu du fichier « prog.py » :

1. print("Hello World !!!")

Il est possible de demander à l’interpréteur d’activer l’optimisation interne de Python avec l’option ‑O passée lors de l’appel. À ce moment‑là, certains tests de vérification comme les assertions sont alors ignorés.

Exemple (depuis un shell Unix) : demande à l'interpréteur Python d'exécuter le script en mode optimisé

1. PromptUnix> python –O prog.py

2. Hello World !!!

Généralement, un script Python commence par la ligne #!/usr/bin/env python. Cette ligne indique au système d'exploitation d'associer le contenu du script à l'interpréteur Python de son environnement. Ce qui permet ensuite d'appeler directement le script comme si c'était un exécutable (plus besoin alors de mettre python devant le nom du script lors de l’appel).

Exemple :

Exécution directe du script :

1. PromptUnix> ./prog.py

2. Hello World !!!

Contenu du fichier « prog.py » :

1. #!/usr/bin/env python

2. print("Hello World !!!")

Toutefois, dans cette forme d’appel, il n’est alors plus possible d’activer l’optimiseur. De plus, quand on exécute un script contenant cette ligne #!/usr/bin/env python, l’interpréteur Python utilisé dans ce cas sera l’interpréteur défini par défaut sur la machine sur laquelle le script est exécuté. Et cet interpréteur est alors fonction des choix de l’administrateur de la machine. Si celui‑ci a choisi Python 2, alors c’est Python 2 qui sera exécuté. Et s’il a positionné Python 3, alors c’est Python 3 qui sera exécuté.

Il est quand même possible de forcer le Python de son choix (s’il est disponible) en spécifiant la version voulue de façon explicite (#!/usr/bin/env python2 ou #!/usr/bin/env python3).

Par ailleurs, un script est considéré comme écrit en ASCII. Si ce n'est pas le cas (comme avec les éditeurs modernes qui ont tous pris l'option « utf‑8 » par défaut), l’interpréteur Python s’arrêtera en échec en arrivant sur une instruction contenant une chaîne accentuée.

Pour lui préciser que le script est encodé en « utf‑8 », il faut rajouter la ligne # coding: utf‑8 ou # ‑*‑ coding: utf‑8 ‑*‑ (ancienne syntaxe) en seconde ligne.

Exemple :

Syntaxe actuelle

1. #!/usr/bin/env python

2. # coding: utf-8

3. print("Hello World !!!")

Ancienne syntaxe (toujours acceptée)

1. #!/usr/bin/env python

2. # -*- coding: utf-8 -*-

3. print("Hello World !!!")

Cette syntaxe n'est bien entendu pas limitée au seul encodage « utf‑8 » et fonctionne avec tous les encodages disponibles de votre OS.

II-5 - Les commentaires

Un commentaire sert à améliorer la lisibilité du programme. Il est placé en le faisant précéder du caractère dièse (#). Tout ce qui suit ce dièse est ignoré jusqu'à la fin de la ligne ; ce qui permet de mettre un commentaire sur une ligne d'instructions. Dans ce cas, il convient alors d’insérer au moins un espace entre la fin de l'instruction et le début du commentaire.

1. #!/usr/bin/env python

2. # coding: utf-8

3. # Ce programme affiche "Hello World !!!"

4. print("Hello World !!!")            # Cette ligne est la ligne qui affiche "Hello World !!!"

Le dièse des deux premières lignes représente d’ailleurs lui aussi un commentaire. En effet, il peut arriver qu’un script Python soit récupéré par un autre. Dans ce cas, les deux premières lignes du script récupéré n’ont plus d’effet dans le script récupérateur (elles ne sont plus les deux premières lignes). Mais n’étant pas des instructions Python (ce sont des lignes habituellement traitées par le système d’exploitation et non par l’interpréteur Python), elles doivent donc être commentées.

II-6 - La fonction « print »

La fonction print() sert à afficher une valeur, une variable, une expression à l’écran. Dans Python 2 elle existe tout aussi bien sous forme de fonction que de simple instruction (ex : print(2) ou print 2), instruction qui disparait dans Python 3 au seul profit de la fonction (print(2)) qui offre plus de souplesse concernant la syntaxe de son paramétrage. Il est alors possible de préciser le séparateur à afficher entre les éléments (séparateur qui par défaut est l’espace), ainsi que le caractère de fin de ligne ('\n' par défaut).

1. >>> print(2, 3)

2. 2 3

3. >>> print(2, 3, sep='xxx')

4. 2xxx3

5. >>> print(2, 3, end='-')

6. 2 3->>>

D’autres possibilités existent comme remplacer l’écran par un flux de sortie différent (fichier, pipe), flusher (vider=écrire) la sortie immédiatement ou pas, etc. Il conviendra de lire la documentation pour étudier ces possibilités plus en détail.

II-7 - le point‑virgule

Généralement, les instructions distinctes se placent sur des lignes distinctes (une instruction par ligne).

1. >>> print(2, 3, sep='+', end='=')

2. 2+3=

3. >>> print(5)

4. 5

5. >>>

Mais il est possible d’écrire plusieurs instructions sur la même ligne en les séparant par un point‑virgule (;). Elles seront alors exécutées séparément.

1. >>> print(2, 3, sep='+', end='='); print(5)

2. 2+3=5

3. >>>

II-8 - l’exception

L’exception (qu’on confond parfois à tort avec l’erreur) est un évènement qui se produit quand Python se trouve dans l’impossibilité d’exécuter une instruction valide. L’impossibilité peut résulter d’une incohérence mathématique (exemple : diviser par 0), d’une incohérence entre l’état d’une donnée et l’état qu’on veut en obtenir (exemple : tenter de convertir une chaîne en nombre) ou de n’importe quel évènement fortuit du système qui se répercute dans Python par ricochet (exemple : disparition du fichier qu’on était en train de traiter).

Dès que Python se trouve en échec, il lève une exception et la fait remonter jusqu’à ce qu’elle soit interceptée. Cela peut se faire dans le code lui‑même (sera vu ultérieurement) sinon elle arrive jusqu’à l’OS qui l’affiche alors sur le terminal (ou la console Python si l’on est dans un environnement de développement) en indiquant la ligne ayant provoqué l’exception ainsi que toute la hiérarchie des appels (la ligne ayant appelé la ligne en question, puis la ligne ayant appelé…, etc.).

1. >>> 1/0

2. Traceback (most recent call last):

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

4. ZeroDivisionError: integer division or modulo by zero

5. >>>

III - Caractéristiques du langage

III-1 - la tabulation

La première caractéristique principale du langage est qu’il est structuré sur les tabulations (ou les espaces). Chaque groupe structurel (alternative, boucle…) doit regrouper les instructions appartenant au groupe dans un bloc tabulé (ou espacé) de façon croissante par rapport aux instructions n’y appartenant pas.

Ainsi, chaque niveau de tabulation correspond à un nouveau bloc, et toute ligne tabulée de façon égale sera alors considérée comme appartenant au même bloc.

Par exemple :

1. #!/usr/bin/env python

2. # coding: utf-8

4. if 5 == 5:

5. print("5 = 5. Belle démonstration !!!")

6. print("Toutefois, chose amusante, 4.99999 (à l’infini) vaut aussi 5…")

Ici, les deux instructions print placées à la même verticale ne seront exécutées qu’à la condition que le test soit vrai.

1. #!/usr/bin/env python

2. # coding: utf-8

4. if 5 == 5:

5. print("5 = 5. Belle démonstration !!!")

6. print("Toutefois, chose amusante, 4.99999 (à l’infini) vaut aussi 5…")

Ici, seul le premier print appartient au bloc if et sera exécuté pour un test vrai ; tandis que le second print sera exécuté dans tous les cas.

Si un nouveau bloc (ex boucle) devait ensuite être intégré dans le bloc if, il serait alors nécessaire de le décaler lui aussi par rapport aux instructions propres au bloc if.

1. #!/usr/bin/env python

2. # coding: utf-8

4. if 5 == 5:

5. i=0

6. while i < 5:

7. print("5 = 5. Belle démonstration !!!")

8. i+=1

9. print("Toutefois, chose amusante, 4.99999 (à l’infini) vaut aussi 5…")

Tandis que si ce nouveau bloc n’appartient pas au bloc if, alors il doit être écrit au même niveau.

1. #!/usr/bin/env python

2. # coding: utf-8

4. if 5 == 5:

5. print("5 = 5. Belle démonstration !!!")

7. i=0

8. while i < 5:

9. print("Toutefois, chose amusante, 4.99999 (à l’infini) vaut aussi 5…")

10. i+=1

Le nombre de tabulations (ou d’espaces) pour un bloc est au choix du développeur. Il peut décider d’en mettre 1, 2, 3, 100, mais ensuite, il devra s’y conformer pour toutes les lignes du bloc. Deux blocs différents peuvent donc ne pas avoir le même nombre d’espaces ou tabulations (ce qui réduira alors bien évidemment la lisibilité du programme), mais toutes les lignes d’un même niveau le doivent.

L’utilisation d’espaces à la place de tabulations est possible, mais le mélange espaces+tabulations est interdit dans Python 3 (il était auparavant autorisé dans Python 2 selon la règle « une tabulation pour 8 espaces » ; ce qui pouvait poser des problèmes de maintenance et de lisibilité, car les développeurs ne sont pas tous obligés de définir la tabulation de leur éditeur sur 8 espaces).

Dans le cas particulier d’un bloc ne comportant qu’une seule instruction, il est possible d’écrire cette instruction directement sur la ligne définissant le bloc.

1. #!/usr/bin/env python

2. # coding: utf-8

4. if 5 == 5: print("5 = 5. Belle démonstration !!!")

Cette notion d’indentation forcée a été une préoccupation première de Guido van Rossum qui était excédé des langages dans lesquels cette syntaxe est permise et même recommandée pour la lisibilité (tel le C), mais que beaucoup de développeurs négligent.

Mais cette syntaxe, dans laquelle ces blocs sans caractère spécifique marquant leur début et leur fin, peut alors donner un effet inverse, surtout pour les blocs longs contenant des blocs imbriqués intermédiaires. Le bloc intermédiaire se termine et on ne sait plus où on en est !

C’est pourquoi les développeurs tentent parfois de trouver des astuces, souvent basées sur les commentaires, pour marquer la fin du bloc afin de mieux le faire ressortir.

1. #!/usr/bin/env python

2. # coding: utf-8

4. if 5 == 5:

5. i=0

6. while i < 5:

7. print("5 = 5. Belle démonstration !!!")

8. i+=1

9. # while

10. # if

11. print("Toutefois, chose amusante, 4.99999 (à l’infini) vaut aussi 5…")

III-2 - la ponctuation

Sa seconde caractéristique est que chaque début de bloc (conditions, boucles, fonctions, objets) se matérialise par un « deux-points » (:) terminant systématiquement l’instruction définissant le bloc.

1. #!/usr/bin/env python

2. # coding: utf-8

4. if 5 == 5:                    # Deux points terminent l’instruction !!!

5. print("5 = 5. Belle démonstration !!!")

6. print("Toutefois 4.99999 (à l’infini) vaut aussi 5. Ne l’oubliez pas…")

Garder en permanence à l’esprit que ces deux caractéristiques sont primordiales dans l’écriture de programmes Python.

IV - Introduction informelle à Python

Dans les exemples qui suivent et jusqu’à la fin de ce tutoriel, les entrées et sorties se distinguent par la présence ou l’absence d’invite (>>> et ...). Pour reproduire les exemples, vous devez taper tout ce qui est après l’invite, au moment où celle-ci apparaît. Notez qu’une invite secondaire affichée seule sur une ligne dans un exemple indique que vous devez entrer une ligne vide ; ceci est utilisé pour terminer une commande multilignes.

Toutefois, pour les codes un peu plus longs, les premières lignes ne seront pas précédées d’invite (>>>) qui ne sera indiquée que sur les 2 ou 3 dernières lignes de l’exemple. Ensuite, à partir de là première ligne contenant une invite (>>>), les lignes qui n’affichent pas d’invite sont les sorties de l’interpréteur.

Et les exemples vraiment très longs ou assez complexes seront écrits sous forme de script Python ce qui sera indiqué par la présence d’une ligne #!/usr/bin/env python en début de l’exemple. Et si le résultat est montré, il le sera alors en dessous dans un bloc séparé du script et intitulé à cet effet.

IV-1 - Les variables

IV-1-a - Préambule

Une variable sert à mémoriser une valeur pour la réutiliser ultérieurement. Son nom reste libre à quelques exceptions près liées aux obligations du langage et de sa syntaxe. Le nom d’une variable doit :

- commencer par une lettre (majuscule ou minuscule) ;

- ne pas contenir de caractères spéciaux tels que étoile, dollar, accolade ; ni de caractère de ponctuation tels la virgule, le point, etc. Le caractère souligné (_) reste cependant autorisé ;

- ne pas contenir d’espace ;

- ne pas être un mot clef de Python.

Une variable se crée en mettant son nom, suivi du caractère égal (=) suivi de la valeur à stocker. Cette valeur peut elle-même provenir d’une autre variable ou d’une expression (expression pouvant faire intervenir elle‑même des variables).

1. >>> a=5

2. >>> b=6

3. >>> c=a*b

4. >>> c

5. 30

L’évaluation de l’expression se faisant toujours avant l’affectation, une variable peut s’utiliser elle‑même pour modifier sa valeur.

1. >>> a=5

2. >>> a=a+1

3. >>> a=a*2

4. >>> a

5. 12

Dans ce cas particulier, on peut alors utiliser un raccourci syntaxique équivalent en remplaçant var=var opérateur valeur par var operateur= valeur (pas d’espace entre operateur et =).

1. >>> a=5

2. >>> a+=1

3. >>> a*=2

4. >>> a

5. 12

IV-1-b - Le typage

Python est un langage dynamiquement typé. Les variables prennent instantanément le type de leur opérande lors de l’affectation ; Python déterminant le type adéquat par la syntaxe de l’opérande utilisé.

La récupération du type d’une variable peut se faire par la fonction type() :

1. >>> a=5

2. >>> b=5.0

3. >>> c="toto"

4. >>> d=b"tutu"

5. >>> type(a)

6. <class 'int'>

7. >>> type(b)

8. <class 'float'>

9. >>> type(c)

10. <class 'str'>

Les types disponibles nativement sont les suivants :

- les entiers (type int). Ils concernent les nombres entiers relatifs (positifs et négatifs). Leur plage de valeur est virtuellement infinie dans Python 3 (ne dépend que de la mémoire de l’ordinateur) tandis qu'elle est limitée dans Python 2 (de l’ordre de -2^62 à 2^62). C’est le type utilisé par défaut lorsque l’opérande est un nombre entier.
Ils peuvent s’écrire sous leur forme naturelle (2, 3, 12, 17, etc.) ; sous forme octale en les faisant précéder de 0o ou 0O (0o2, 0o3, 0O10, 0O21, etc.) ; sous forme hexadécimale en les faisant précéder de 0x ou 0X (0x2, 0x3, 0XB, 0X11) ; ou même sous forme binaire en les faisant précéder de 0b ou 0B (0b10, 0b11, 0B1100, 0B100001).
De plus, cette écriture peut s’agrémenter d’un ou plusieurs soulignés (_) permettant de faciliter la lecture du nombre si on l’emploie par exemple pour représenter le séparateur des milliers, millions (123_456_789 au lieu de 123456789),

- les entiers (type long). Ce type spécifique à Python 2 et disparu dans Python 3 (car intégré dans les int) concerne les grands nombres entiers relatifs (positifs et négatifs). Leur plage de valeur est virtuellement infinie (ne dépend que de la mémoire de l’ordinateur). Ce type est appliqué si l’opérande contient un nombre entier suivi de la lettre L ou l (2l, 3l, 17L, etc.) ;

- les nombres à virgule flottante (type float). Ils concernent les nombres décimaux. Leur nom vient du fait que pour deux décimaux distincts, la virgule décimale n’est pas nécessairement à la même position (elle « flotte »). Ce type est appliqué dès que l’opérande contient un nombre incluant un point (séparateur décimal anglo‑saxon). Leur plage de valeur va d’environ 10‑308 jusqu’à environ 10^308 (y compris dans les négatifs). Ce type n’offre pas d’écriture sous forme binaire, octale ou hexadécimale, mais offre toutefois le séparateur souligné (2.5, 3.17, 123_456.05, etc.) ;

- les chaînes de caractères (type str), les tuples (type tuple), les listes (type list), les ensembles (type set), les dictionnaires (type dict), les booléens (type bool), les none (type NoneType). Ils seront vus plus en détail ultérieurement.

Tous les types natifs Python (int, float, str, etc.) possèdent des fonctions portant le même nom permettant alors de forcer ou caster (to cast=jouer un rôle) un type particulier à la place du type pris par défaut. Il suffit d'appeler la fonction du type demandé en y passant la valeur à forcer.

1. >>> a=5

2. >>> b=float(a)

3. >>> c=str(b)

4. >>> d=bytes(c)

5. >>> type(a)

6. <class 'int'>

7. >>> type(b)

8. <class 'float'>

9. >>> type(c)

10. <class 'str'>

En plus des nombres entiers et décimaux, Python gère les nombres complexes, en utilisant le suffixe j ou J pour indiquer la partie imaginaire (exemple a=3+5j). Et il existe des librairies spécifiques pour gérer les nombres décimaux avec exactitude (ce point sera décrit ultérieurement) et les nombres rationnels.

IV-1-c - Les annotations

Depuis la version 3.5 de Python, les variables peuvent être annotées par le programmeur, c’est-à-dire qu’il peut y rajouter le type qu’il a prévu pour la variable.

Cela se fait en rajoutant :nom_du_type_attendu après le nom de la variable et avant l’opérateur = de l’affectation.

Exemple :

Syntaxe habituelle

1. a=5

Nouvelle syntaxe possible

1. a:int=5

De plus, cette syntaxe accepte plusieurs types à mettre dans un tuple.

1. a:(int,float)=5

2. b:(int,float)=6.5

Cette notation est facultative, mais si elle est appliquée, le typage indiqué doit correspondre à une liste de mots clefs précis permettant d’englober tous les types connus de Python 3. On trouvera donc int, float, bytes, str, bool, tuple, list, set, dict et None correspondants aux types des mêmes noms ; auxquels viendront se greffer callable représentant un paramètre appelable (généralement une fonction), object représentant un paramètre de type objet (sera vu ultérieurement) et any indiquant un paramètre de type indéterminé ou changeable. Cette liste peut aussi contenir les noms des objets personnels (sera vu ultérieurement) créés en amont dans le code.

Néanmoins ces indications sont, pour l’instant, purement informatives. Python ne contrôle donc pas la correspondance entre ce qui est indiqué comme type prévu et ce qui est réellement affecté dans la variable, mais cela risque de changer dans les versions futures.

1. a:int="toto"                 # Une string là où il est annoté un int

2. b:str=123                    # Un int là où il est annoté une string

IV-2 - Les opérateurs

IV-2-a - Arithmétiques et logiques

Les opérateurs arithmétiques et logiques permettent d’effectuer des opérations (le plus souvent mathématiques). On trouve :

- l’addition (+). Ex : 2+3 => 5 ;

- la soustraction (-). Ex : 3‑2 => 1 ;

- la multiplication (*). Ex : 3*4 => 12 ;

- la division (/). Pour cette dernière, dans Python 2, l’opération s’effectue selon le type le plus large des opérandes à diviser (pour deux entiers cela sera alors une division entière), tandis que dans Python 3, quel que soit le type des opérandes (tout en restant des numériques) l’opération est une vraie division décimale. Ex 11/4 => 2 sous Python 2 et 2.75 sous Python 3 ;

- la division euclidienne (//). Division calculée dans le type le plus large des opérandes à diviser (pour deux entiers cela sera alors une division entière), mais donnant quoiqu'il arrive un résultat arrondi à l’entier inférieur. Ex : 11//4 => 2 et 11.0//4 => 2.0 ;

- le modulo (%) qui est le reste d’une division entière. Ex : 11%4 => 3 ;

- la puissance (**) appelée aussi « exposant ». Ex : 11**4 => 14641. Elle peut s’utiliser avec des opérandes flottants (ce qui donne des racines nièmes) ainsi que sur des négatifs (ce qui peut alors donner des nombres complexes) ;

- le « non » bit à bit (~), qui donne un nombre où tous les bits de l'opérande ont été inversés. Ex : ~123 => ‑124 (et effectivement, avec la façon de coder les négatifs en informatique aujourd’hui, le résultat de ~x est égal à –x‑1) ;

- le « et » bits à bits (&), qui donne un nombre composé des bits à 1 communs entre l’opérande de gauche et celui de droite. Ex : 6&5 => 4 ;

- le « ou inclusif » bits à bits (|), qui donne un nombre composé de tous les bits à 1 présents dans les deux opérandes. Ex : 6|5 => 7 ;

- le « ou exclusif » bits à bits (^), qui donne un nombre composé des bits à 1 présents uniquement dans l'un des deux opérandes au choix. Ex : 6^5 => 3. Cet opérateur peut aussi se retrouver par composition du not, du & et du | (a^b=a&(~b) | (~a)&b). Ex : 6&(~5) | (~6)&5 => 3 ;

- le décalage de bits à gauche (<<) ou à droite (>>), ce qui équivaut respectivement à une multiplication ou une division euclidienne par 2 élevée à la puissance du décalage. Ex : 11<<4 => 176 (tout comme 11*(2**4)) et 14641>>3 => 1830 (tout comme 14641//(2**3)).

Le résultat des opérations dépend du type des opérandes impliqués et, sauf cas particulier (comme la division dans Python 3 qui pourra donner un flottant même si on ne divise que des entiers), sera toujours du type le plus large des opérandes impliqués. Dans certains cas, il peut même ne plus rien à voir avec une opération mathématique, tout en restant cohérent dans une autre logique (par exemple multiplier une chaîne par un nombre répètera la chaîne autant de fois que le nombre indiqué).

1. >>> 2 + 2                  # int + int => int

2. 4

3. >>> 50 – 5 * 6             # int – int * int => int

4. 20

5. >>> (50 - 5.0 * 6) // 4    # (int – float * int) // int => float

6. 5.0

7. >>> 17 // 3                # int // int => int

8. 5

9. >>> 17 // 3.0              # int / float => float (mais la division est restée entière)

10. 5.0

11. >>> 17 / 3                 # int / int => float (cas particulier de la division Python 3)

12. 5.666666666666667

13. >>> 17 % 3                 # reste de la division

14. 2

15. >>> 5 * 3 + 2              # quotient * diviseur + reste => redonne le nombre initial

16. 17

IV-2-b - Relationnels

Les opérateurs relationnels permettent d’effectuer des comparaisons. On trouve :

- la comparaison d'égalité (== ou !=) qui donne un booléen vrai/faux selon que les opérandes sont égaux ou différents dans leur valeur (et inversement) ;

- la relation d'ordre (<, >, <=, >=) qui donne un booléen vrai/faux selon que l’opérande de gauche est plus petit, plus grand, plus petit ou égal, plus grand ou égal (et inversement) que l’opérande de droite ;

- le is qui vérifie si l’opérande de gauche est identique (au sens interne de Python) à l’opérande de droite ;

- arrivé avec la version 3.8, l'opérateur morse (:=) permet de faire simultanément une affectation et un test dans la même expression. Ainsi, ce qui dans une version précédente se serait écrit v=expression(); if v == valeur: action, peut s'écrire maintenant if v:=expression() == valeur: action. Le nom de cet opérateur vient de sa ressemblance avec les yeux et les dents de l’animal.

IV-2-c - Connecteurs

Les connecteurs peuvent associer ou modifier le sens de l'évaluation. On trouve :

- le and (vf « et »), qui ne validera l'expression entière que si les expressions situées de part et d'autre sont elles‑mêmes validées,

- le or (vf « ou »), qui validera l'expression entière si au moins l'une des expressions situées de part et d'autre est elle‑même validée.

Ces deux connecteurs fonctionnent à l'économie. L'évaluation de l'expression s'arrête donc dès qu'une certitude est atteinte dans un sens ou dans l'autre (ex : X or Y or Z s'arrête dès le premier True trouvé),

- le not, qui inverse le sens booléen de l'expression.

À noter : il est possible de chaîner plusieurs opérateurs relationnels. Exemple : a < b < c < d renverra vrai si toutes les valeurs sont rangées par ordre croissant (Python remplace automatiquement ce type d'instruction en a < b and b < c and c < d). Cela permet de vérifier si une expression est incluse entre deux valeurs sans avoir à la répéter (expr > x and expr < y devenant alors x < expr < y).

IV-2-d - Priorités

Les opérateurs suivent des règles de priorité équivalentes à celles de l’arithmétique. Dans l’ordre croissant la comparaison ; puis les opérateurs bits à bits ; puis le décalage, puis l’addition et la soustraction ; puis la multiplication et la division ; puis les puissances. Et à opérateurs de priorité égale, les opérations se font de la gauche vers la droite…

1. >>> 4 ** 2

2. 16

3. >>> 16 * 3

4. 48

5. >>> 48 + 2

6. 50

7. >>> 2 << 50

8. 2251799813685248

9. >>> 1 | 2251799813685248

10. 2251799813685249

11. >>> 1 != 2251799813685249

12. True

13. >>> 1 != 1 | 2 << 2 + 3 * 4 ** 2

14. True

… à moins que des parenthèses ne viennent modifier cet ordre naturel.

1. >>> 1 != 1

2. False

3. >>> False | 2

4. 2

5. >>> 2 << 2

6. 8

7. >>> 8 + 3

8. 11

9. >>> 11 * 4

10. 44

11. >>> 44 ** 2

12. 1936

13. >>> (((((1 != 1) | 2) << 2) + 3) * 4) ** 2

14. 1936

IV-3 - Les chaînes de caractères

Au‑delà des nombres, Python sait aussi manipuler des chaînes de caractères, qui peuvent être exprimées de différentes manières. Elles peuvent être écrites entre quotes simples ('...') ou entre guillemets ("...") sans distinction. Et le backslash (\) permet de protéger le caractère d’encadrement lorsqu’il doit apparaître comme caractère de la chaîne.

1. >>> 'œufs et pain'            # quotes simples

2. 'œufs et pain'

3. >>> 'aujourd\'hui'            # le backslash protège la quote simple…

4. 'aujourd’hui'

5. >>> "aujourd'hui"             # …ou plus simplement utiliser les guillemets

6. 'aujourd’hui'

7. >>> '"Oui", dit-il.'

8. '"Oui", dit-il.'

9. >>> "\"Oui\", dit-il."

10. '"Oui", dit-il.'

Les chaînes peuvent être concaténées (collées) avec l’opérateur + (plus) et répétées avec l’opérateur * (étoile).

1. >>> 'Barba' + 2 * 'pa'        # 'Barba' suivi de 2 fois 'pa'

2. 'Barbapapa'

Les chaînes de caractères peuvent être indicées (accéder aux caractères par leurs positions) en utilisant la notation « crochets » utilisée généralement dans les tableaux (chaine[position]). Le premier caractère d’une chaîne est à la position 0.

Afin de simplifier les choses, il n’existe pas de type distinct pour les caractères. Un caractère est simplement une chaîne de longueur 1.

1. >>> word='Python'

2. >>> word[0]                   # Caractère à la position 0 (le premier)

3. 'P'

4. >>> word[5]                   # Caractère à la position 5 (le sixième)

5. 'n'

Les indices peuvent également être négatifs ce qui les décompte alors en partant de la fin.

1. >>> word[-1]                 # Dernier caractère

2. 'n'

3. >>> word[-2]                 # Avant-dernier caractère

4. 'o'

5. >>> word[-6]                 # Sixième en partant de la fin

6. 'P'

Notez que ‑0 ne pouvant pas être utilisé pour partir de la fin (car il est égal à 0), les indices négatifs commencent à ‑1.

En plus de l’indexation utilisée pour obtenir les caractères individuellement, le découpage (slicing) permet d’obtenir une sous-chaîne par extraction de tous les caractères situés entre deux positions.

1. >>> word[0:2]                # De la position 0 (incluse) à 2 (exclue)

2. 'Py'

3. >>> word[2:5]                # De la position 2 (incluse) à 5 (exclue)

4. 'tho'

Le saut entre les positions peut être spécifié en 3e paramètre…

1. >>> word[1:6:2]              # De la position 1 (incluse) à 6 (exclue) par pas de 2

2. 'yhn'

… ce qui permet alors de traiter la chaîne dans le sens inverse.

1. >>> word[5:0:-2]             # De la position 5 (incluse) à 0 (exclue) par pas de ‑2

2. 'nhy'

Notez que le début ou la fin (ou les deux) peuvent être omis (ou mis à None). Si le début est omis ou None, alors le découpage commence au premier caractère inclus. Si la fin est omise, alors le découpage s’arrête au dernier caractère inclus. De plus, la notion « premier » ou « dernier » dépend du sens de traitement donné par le 3e paramètre.

1. >>> word[::2]                # Du début à la fin par pas de 2

2. 'Pto'

3. >>> word[::-2]               # De la fin vers le début par pas de ‑2

4. 'nhy'

Inclure le début et exclure la fin permet que chaine[:i] + chaine[i:] soit toujours égal à chaine.

1. >>> word[:2] + word[2:]

2. 'Python'

3. >>> word[None:4] + word[4:None]

4. 'Python'

Les chaînes de caractères, en Python, ne peuvent pas être modifiées. On dit qu’elles sont immuables (par opposition aux types « mutables »). Tenter de placer un caractère quelconque à un indice ou à un slice dans une chaîne produit une exception.

1. >>> word[0]='J'

2. TypeError: 'str' object does not support item assignment

3. >>> word[2:]='py'

4. TypeError: 'str' object does not support item assignment

Le seul moyen de remplacer tout ou partie d’une chaîne est d’en créer une nouvelle à partir de la première, quitte ensuite à la réaffecter dans la même variable.

1. >>> 'J' + word[1:]

2. 'Jython'

3. >>> word=word[2:] + 'py'            # Rappel: la variable d’origine contient "Python"

4. >>> word

5. 'thonpy'

La fonction native len() renvoie la longueur d’une chaîne (nombre de caractères).

1. >>> s='supercalifragilisticexpialidocious'

2. >>> len(s)

3. 34

L'opérateur in (inverse not in) permet de vérifier l'existence (ou la non‑existence) d'une sous-chaîne dans une chaîne.

1. >>> s='abracadabra'

2. >>> 'bra' in s

3. True

4. >>> 'ca' not in s

5. False

IV-4 - Les tuples

Python connaît différents types de données combinées, utilisés pour regrouper plusieurs valeurs. Le plus simple à appréhender est le tuple, qui peut être représenté comme une suite de valeurs (éléments) séparées par des virgules. Il s’apparente aux tableaux des autres langages.

Les éléments sont ordonnés (la restitution se fait dans le même ordre que l’ordre de création).

Les éléments d’un tuple ne sont pas obligatoirement tous du même type, bien qu’à l’usage ce soit souvent le cas.

1. >>> squares=1, 4, 9, 16, 25, "toto"

2. >>> type(squares)

3. <class 'tuple'>

4. >>> squares

5. (1, 4, 9, 16, 25, 'toto')

Comme on peut le voir, Python rajoute des parenthèses quand il affiche ses tuples afin de bien les faire ressortir. L’usage de parenthèses à la création de tuples est aussi possible et même vivement recommandé pour éviter les erreurs de syntaxe.

1. >>> squares=(1, 4, 9, 16, 25, "toto")

2. >>> type(squares)

3. <class 'tuple'>

Il devient d'ailleurs obligatoire quand on veut regrouper certains sous‑ensembles comme stocker des tuples dans un tuple.

1. >>> domino=((0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2))

2. >>> type(domino)

3. <class 'tuple'>

4. >>> domino

5. ((0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2))

C’est pourquoi le programmeur Python prend rapidement l’habitude de toujours encadrer ses tuples de parenthèses. Mais notez bien que c’est la virgule qui fait le tuple et non les parenthèses.

1. >>> a=(1)

2. >>> type(a)

3. <class 'int'>

4. >>> b=(1,)

5. >>> type(b)

6. <class 'tuple'>

Comme les chaînes de caractères (et tout autre type de séquence), les tuples peuvent être indicés et découpés.

1. >>> squares=(1, 4, 9, 16, 25, "toto")

2. >>> squares[0]                      # index (renvoie un item)

3. 1

4. >>> squares[-1]

5. 'toto'

6. >>> squares[-3:]                    # slice (renvoie un nouveau tuple)

7. (16, 25, 'toto')

Toutes les opérations par tranches renvoient un nouveau tuple copié à partir du premier.

1. >>> squares[:]

2. (1, 4, 9, 16, 25, 'toto')

De même que pour les chaînes, les tuples acceptent aussi certaines opérations comme la concaténation…

1. >>> squares=(1, 4, 9, 16, 25, "toto")

2. >>> squares + (36, 49, 64, 81, 100)

3. (1, 4, 9, 16, 25, 'toto', 36, 49, 64, 81, 100)

…ou la multiplication.

1. >>> datas=(2, 3) * 5

2. >>> datas

3. (2, 3, 2, 3, 2, 3, 2, 3, 2, 3)

De même que pour les chaînes, les tuples sont immuables : il est impossible de changer leur contenu…

1. >>> squares=(1, 4, 9, 16, 25, "toto")

2. >>> squares[0]=0

3. TypeError: 'tuple' object does not support item assignment

…mais de même que pour les chaînes, il est possible de remplacer un tuple déjà existant par un autre en jouant avec les opérations de Python pour récupérer les éléments voulus.

1. >>> squares=(1, 4, 9, 16, 25, "toto")

2. >>> squares=(0,) + square[:-1]

3. >>> squares

4. (0, 1, 4, 9, 16, 25)

La primitive len() et les opérateurs in/not in s’appliquent aussi aux tuples.

1. >>> letters=('a', 'b', 'c', 'd')

2. >>> len(letters)

3. 4

4. >>> "c" in letters

5. True

Puisque chaque élément d’un tuple peut être n’importe quel élément Python, il est donc possible d’imbriquer des tuples (de créer des tuples contenant d’autres tuples)…

1. >>> alpha=('a', 'b', 'c')

2. >>> num=(1, 2, 3)

3. >>> x=(alpha, num)

4. >>> x

5. (('a', 'b', 'c'), (1, 2, 3))

6. >>> type(x)

7. <class 'tuple'>

8. >>> x[0]

9. ('a', 'b', 'c')

10. >>> type(x[0])

11. <class 'tuple'

12. >>>> x[1]

13. (1, 2, 3)

14. >>> type(x[1])

15. <class 'tuple'

16. >>> x[0][1]

17. 'b'

18. >>> x[1][2]

19. 3

… ce qui ouvre la voie à l’utilisation de containers multidimensionnels.

Le contenu d’un tuple peut être distribué dans des variables distinctes (unpacking) à condition d’avoir autant de variables que ce que le tuple contient d’éléments.

1. >>> n=(1, 2, 3)

2. >>> (x, y, z)=n

3. >>> x

4. 1

5. >>> y

6. 2

7. >>> z

8. 3

Ce mécanisme peut être judicieusement utilisé pour permuter deux variables sans passer par une troisième.

1. >>> x=123

2. >>> y=456

3. >>> (x, y)

4. (123, 456)

5. >>> (x, y)=(y, x)

6. >>> (x, y)

7. (456, 123)

Tout comme pour les types standards, le tuple possède sa propre fonction tuple() permettant de caster (forcer) une expression en tuple.

1. >>> squares=tuple((1, 4, 9, 16, 25, "toto"))

2. >>> type(squares)

3. <class 'tuple'>

On remarquera que tuple() étant une fonction, il faut lui mettre des parenthèses pour demander son exécution. Mais son paramètre étant ici un tuple (qui lui aussi utilise des parenthèses pour sa syntaxe de création), cela conduit alors l’instruction à avoir deux jeux de parenthèses dans sa syntaxe.

Alors OK ça semble idiot de forcer un tuple à être un tuple, mais on verra plus tard qu'il existe des objets susceptibles de contenir plusieurs éléments et là le cast deviendra utile pour forcer ces éléments à apparaître concrètement.

IV-5 - Les listes

La liste peut être vue comme une extension du tuple qui autorise le rajout, la modification ou la suppression d’éléments. Celui qui sait manipuler le tuple peut alors aisément se tourner vers la liste qui fournira les mêmes services de base tout en y rajoutant les outils permettant sa modification.

La syntaxe de création d’une liste est la même que pour le tuple, excepté que les éléments sont encadrés de crochets et non de parenthèses. De plus, contrairement au tuple où les parenthèses sont facultatives (mais fortement conseillées), les crochets pour les listes sont obligatoires (sinon on retombe dans la syntaxe du tuple).

1. >>> squares=[1, 4, 9, 16, 25, "toto"]

2. >>> type(squares)

3. <class 'list'>

4. >>> squares

5. [1, 4, 9, 16, 25, 'toto']

Comme les chaînes de caractères et les tuples (et tout autre type de séquence), les listes peuvent être indicées et découpées.

1. >>> squares[0]                          # index (renvoie un item)

2. 1

3. >>> squares[-1]

4. 'toto'

5. >>> squares[-3:]                        # slice (renvoie une nouvelle liste)

6. [16, 25, 'toto']

Mais à la différence des chaînes et tuples qui sont immuables, les listes sont mutables : il est donc possible de changer leur contenu.

1. >>> cubes=[1, 8, 27, 65, 125]           # quelque chose ne va pas…

2. >>> 4 ** 3                              # le cube de 4 est 64, pas 65!

3. 64

4. >>> cubes[3]=64                         # on remplace l’élément incorrect

5. >>> cubes

6. [1, 8, 27, 64, 125]

S'il est possible de modifier un élément par son indice, il n’est toutefois pas possible de créer un nouvel élément en lui affectant un indice n’existant pas encore. Le rajout d’éléments peut se faire un par un avec la méthode append() ou peut se faire par rajout d’un itérable quelconque (donc chaîne, tuple, liste ou autre) avec la méthode extend().

Dans les deux cas, les éléments ajoutés viennent se mettre en fin de liste.

1. >>> cubes.append(343)

2. >>> cubes.append(8 ** 3)

3. >>> cubes

4. [1, 8, 27, 64, 125, 343, 512]

5. >>> cubes.extend([729, 10 ** 3])

6. >>> cubes.extend((11 ** 3,))

7. >>> cubes

8. [1, 8, 27, 64, 125, 343, 512, 729, 1000, 1331]

Il est aussi possible d’insérer un élément à une position particulière avec la méthode insert(). L’élément se place à la position demandée en décalant tout le reste de la liste.

1. >>> cubes[5]

2. 343

3. >>> cubes.insert(216, 5)

4. >>> cubes

5. [1, 8, 27, 64, 125, 216, 343, 512, 729, 1000, 1331]

6. >>> cubes[5]

7. 216

8. >>> cubes[6]

9. 343

Il est possible de supprimer un ou plusieurs éléments avec l’opérateur del(), qui peut s'utiliser avec ou sans parenthèses. Dans ce cas, les éléments placés à droite des éléments supprimés sont alors décalés pour coller aux éléments à gauche (ce qui n’est pas le cas dans d’autres langages comme le php).

1. >>> del(cubes[0])

2. >>> cubes

3. [8, 27, 64, 125, 216, 343, 512, 729, 1000, 1331]

4. >>> cubes[6]

5. 512

6. >>> del cubes[0:3]

7. >>> cubes

8. [125, 216, 343, 512, 729, 1000, 1331]

9. >>> cubes[6]

10. 1331

11. >>> cubes[3]

12. 512

Des affectations de tranches sont également possibles, ce qui peut même modifier la taille de la liste ou la vider complètement :

1. >>> letters=['a', 'b', 'c', 'd', 'e', 'f', 'g']

2. >>> letters

3. ['a', 'b', 'c', 'd', 'e', 'f', 'g']

4. >>> letters[2:5]=['C', 'D', 'E']         # remplacer quelques valeurs par d’autres valeurs

5. >>> letters

6. ['a', 'b', 'C', 'D', 'E', 'f', 'g']

7. >>> letters[2:5]=[]                      # remplacer quelques valeurs par une liste vide

8. >>> letters

9. ['a', 'b', 'f', 'g']

10. >>> letters[:]=[]                        # remplacer toutes les valeurs par une liste vide

11. >>> letters

12. []

Tout comme pour le tuple et les autres types, la liste possède sa propre fonction list() permettant de caster (forcer) une expression en liste.

1. >>> squares=list((1, 4, 9, 16, 25, "toto"))

2. >>> type(squares)

3. <class 'list'>

De même que pour le cast en tuple, list() étant une fonction, il faut lui mettre des parenthèses pour demander son exécution. Cependant, son paramètre étant ici un tuple (qui lui aussi utilise des parenthèses pour sa syntaxe de création), cela conduit alors l’instruction à avoir deux jeux de parenthèses dans sa syntaxe.

IV-6 - Les ensembles

Python fournit un type de donnée pour les ensembles. Un ensemble est une collection d’éléments ne comportant aucun élément dupliqué (tout élément dupliqué est ignoré lors de la création de l’ensemble ou lors des insertions).

Les éléments sont non ordonnés (l’ordre de restitution ne correspond pas forcément à l’ordre de création). De fait, les ensembles ne permettent pas les accès directs au travers d’indices comme avec les tuples ou les listes.

À noter : un ensemble ne peut contenir que des éléments immuables.

Des utilisations basiques concernent par exemple des tests d’appartenance ou des suppressions de doublons, qui sont bien plus efficaces que sur les listes, car il n’est pas nécessaire de tout traiter pour obtenir un résultat.

Les ensembles supportent également les opérations mathématiques comme les unions, intersections, différences et différences symétriques.

La syntaxe de création d’un ensemble est similaire à celle d’un tuple ou d’une liste excepté que les éléments sont encadrés d’accolades {}.

1. >>> fruit={'pomme', 'orange', 'pomme', 'abricot', 'orange', 'banane'}

2. >>> type(fruit)

3. <class 'set'>

4. >>> fruit

5. set(['orange', 'banane', 'abricot', 'pomme'])        # Les éléments dupliqués ont disparu

Le rajout d’éléments peut se faire un par un avec la méthode add() ou peut se faire par rajout d’un itérable quelconque (donc chaîne, tuple, liste ou autre) avec la méthode update().

Dans tous les cas, si la demande implique l’ajout d’un élément déjà présent, l’élément est alors simplement ignoré.

1. >>> fruit.add('poire')

2. >>> fruit.add('poire')

3. >>> fruit

4. set(['orange', 'banane', 'abricot', 'pomme', 'poire'])

5. >>> fruit.update(('kiwi', 'pomme', 'prune', 'kiwi'))

6. >>> fruit

7. set(['orange', 'banane', 'abricot', 'pomme', 'poire', 'kiwi', 'prune'])

Manipulations d'ensembles

1. >>> a={'a', 'b', 'r', 'a', 'c', 'a', 'd', 'a', 'b', 'r', 'a'}

2. >>> b=set('alacazam')        # Plus facile non ?

3. >>> a

4. {'c', 'b', 'd', 'a', 'r'}    # L’ordre initial n’est pas garanti

5. >>> b

6. {'c', 'm', 'a', 'l', 'z'}

7. >>> a – b                    # Lettres dans a qui ne sont pas dans b (b ôté de a)

8. {'d', 'r', 'b'}

9. >>> a | b                    # Lettres dans a ou dans b

10. {'c', 'b', 'd', 'm', 'a', 'r', 'l', 'z'}

11. >>> a & b                    # Lettres à la fois dans a et dans b

12. {'a', 'c'}

13. >>> a ^ b                    # Lettres dans a ou b mais pas dans les deux ((a | b) – (a & b))

14. {'d', 'b', 'm', 'r', 'l', 'z'}

La primitive len() et les opérateurs in/not in s’appliquent aussi aux ensembles.

1. >>> letters={'a', 'b', 'c', 'd'}

2. >>> len(letters)

3. 4

4. >>> "c" in letters

5. True

Tout comme pour le tuple ou la liste et les autres types, l'ensemble possède sa propre fonction set() permettant de caster (forcer) une expression en ensemble.

1. >>> fruit=set(('pomme', 'orange', 'pomme', 'abricot', 'orange', 'banane'))

2. >>> type(fruit)

3. <class 'set'>

De même que pour le cast en tuple ou en liste, la fonction set() étant une fonction, il faut lui mettre des parenthèses pour demander son exécution. Toutefois son paramètre étant ici un tuple (qui lui aussi utilise des parenthèses pour sa syntaxe de création), cela conduit alors l’instruction à avoir deux jeux de parenthèses dans sa syntaxe.

IV-7 - Les dictionnaires

Le dernier type d’agrégation de données de Python est le dictionnaire ; présent parfois dans d’autres langages sous le nom de « mémoire associative » ou de « tableau associatif ».

À la différence des chaînes, tuples et listes ; qui sont indicés par le rang de leurs éléments (les ensembles, eux, n'étant pas indicés du tout), les dictionnaires sont indexés par des clefs, qui, comme les ensembles, ne peuvent être composées que d’éléments immuables.

Le plus simple est de considérer les dictionnaires comme des ensembles de paires clefs+valeur, chaque clef devant être unique au sein d’un dictionnaire.

Jusqu’à Python 3.7, les éléments étaient non ordonnés (l’ordre de restitution ne correspondant pas forcément à l’ordre de création). Depuis Python 3.7, les éléments sont désormais ordonnés par l’ordre de création. Les exemples de ce tutoriel ne seront toutefois pas toujours en adéquation avec cette récente directive.

La syntaxe de création d’un dictionnaire se fait en encadrant les éléments par des accolades {}. Les éléments se placent par paires clef : valeur séparées par des virgules. C’est également de cette façon que les dictionnaires sont affichés en sortie.

Les principales opérations effectuées sur un dictionnaire consistent à stocker une valeur pour une clef et à extraire la valeur correspondant à une clef. On accède aux éléments d’un dictionnaire de la même façon qu’aux éléments de tuples et listes : on indique le nom du dictionnaire puis on place la clef de l’élément voulu entre crochets. On peut donc de cette façon accéder aux éléments existants, mais aussi créer de nouveaux éléments à la volée…

1. >>> tel={"jack" : 4098, "paul" : 4139}

2. >>> tel["guido"]=4127

3. >>> type(tel)

4. <class 'dict'>

5. >>> tel

6. {'paul': 4139, 'guido': 4127, 'jack': 4098}

7. >>> tel["jack"]

8. 4098

… voire même écraser des éléments déjà existants.

1. >>> tel={"jack" : 4098, "paul" : 4139}

2. >>> tel["jack"]=4099

3. >>> tel["jack"]

4. 4099

Il est également possible de supprimer une paire clef : valeur avec l’opérateur del().

1. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

2. >>> tel["irv"]=4700

3. >>> del tel["paul"]

4. >>> tel

5. {'guido' : 4127, 'irv': 4700, 'jack': 4098}

La méthode keys() d’un dictionnaire renvoie une vue de toutes les clefs utilisées dans le dictionnaire, dans un ordre correspondant à l’ordre interne des éléments. La méthode values() renvoie une vue de toutes les valeurs (a priori dans le même ordre que keys()) et la méthode items() renvoie une vue de tuples clef+valeur (là encore à priori dans le même ordre).

1. >>> tel.keys()

2. dict_keys(['guido', 'irv', 'jack'])

3. >>> tel.values()

4. dict_values([4127, 4700, 4098])

5. >>> tel.items()

6. dict_items([('guido', 4127),( 'irv', 4700), ('jack', 4098)])

Ces vues s'adaptent au dictionnaire en temps réel. C'est-à-dire que si le dictionnaire évolue, la vue évolue avec.

1. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

2. >>> k=tel.keys()

3. >>> k

4. dict_keys(['paul', 'jack', 'guido'])

5. >>> del tel["paul"]

6. >>> k

7. dict_keys(['jack', 'guido'])

À noter : dans Python 2, ces méthodes ne renvoient pas des vues, mais des listes figées qui n'évoluent donc plus après avoir été invoquées.

1. >>> # Exemple Python 2

2. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

3. >>> k=tel.keys()

4. >>> k

5. ['paul', 'jack', 'guido']

6. >>> del tel["paul"]

7. >>> k

8. ['paul', 'jack', 'guido']

Dans Python 2, les vues existent aussi et se nomment viewkeys(), viewvalues() et viewitems().

1. >>> # Exemple Python 2

2. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

3. >>> tel.viewkeys()

4. dict_keys(['guido', 'irv', 'jack'])

5. >>> tel.viewvalues()

6. dict_values([4127, 4700, 4098])

7. >>> tel.viewitems()

8. dict_items([('guido', 4127),( 'irv', 4700), ('jack', 4098)])

Python 2 offre aussi un autre outil pour les dictionnaires : les itérateurs. Ceux-ci se nomment iterkeys(), itervalues() et iteritems().

1. >>> # Exemple Python 2

2. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

3. >>> tuple(tel.iterkeys())

4. ('guido', 'irv', 'jack')

5. >>> tuple(tel.itervalues())

6. (4127, 4700, 4098)

7. >>> tuple(tel.iteritems())

8. (('guido', 4127),( 'irv', 4700), ('jack', 4098))

Toutefois, contrairement aux vues (qui se synchronisent avec le dictionnaire) ou aux listes (qui, une fois créées, se désolidarisent du dictionnaire dont elles sont issues), d’une part ces objets ne sont utilisables qu’une seule fois…

1. >>> # Exemple Python 2

2. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

3. >>> k=tel.iterkeys()

4. >>> tuple(k)

5. ('guido', 'irv', 'jack')

6. >>> tuple(k)

7. ())

… et d’autre part ces objets ne sont plus utilisables si le dictionnaire dont ils sont issus évolue avant leur utilisation.

1. >>> # Exemple Python 2

2. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

3. >>> k=tel.iterkeys()

4. >>> del tel["jack"]

5. >>> tuple(k)

6. RuntimeError: dictionary changed size during iteration

Devant les désagréments de ces limitations, il est compréhensible alors que ces méthodes ont disparu dans Python 3 au profit des vues offrant la plus ouverte et la plus souple des différentes technologies.

La fonction len() et les opérateurs in/not in s’appliquent aussi aux dictionnaires (plus exactement ils s’appliquent à ses clefs).

1. >>> tel={"jack": 4098, "paul": 4139, "guido" : 4127}

2. >>> len(tel)

3. 3

4. >>> "guido" in tel

5. True

Tout comme pour le tuple, la liste ou l'ensemble et les autres types, le dictionnaire possède sa propre fonction dict() permettant de caster (forcer) une expression en dictionnaire.

1. >>> tel=dict((("jack", 5), ("paul", 6)))

2. type(tel)

3. <class 'dict'>

De même que pour le cast en tuple, en liste ou en ensemble, dict() étant une fonction, il faut lui mettre des parenthèses pour demander son exécution. Cependant, son paramètre étant ici un tuple (qui lui aussi utilise des parenthèses pour sa syntaxe de création), et chaque élément du tuple étant un couple qui sera traité sous forme de clef+valeur, cela conduit alors l’instruction à avoir trois jeux de parenthèses dans sa syntaxe (premier jeu pour la fonction elle-même, second jeu pour le tuple, et troisième jeu pour chaque couple dudit tuple).

À noter : les accolades pouvant servir tout aussi bien pour symboliser un ensemble qu’un dictionnaire, des accolades vides symbolisent par défaut un dictionnaire. Pour créer un ensemble vide, il faut donc impérativement utiliser la fonction set(). Et de façon plus générale, il est plus prudent et plus lisible (explicite vaut mieux qu’implicite) de passer explicitement par les fonctions nominatives pour créer un container (tuple, liste, ensemble, dictionnaire) vide.

IV-8 - True, False, None

Les valeurs True et False (de type bool) servent à représenter des booléens qui peuvent être utilisés lors de tests de vérification d'une particularité sur des ensembles. Ce sont aussi les valeurs représentant une évaluation logique. Il est alors possible de récupérer ces valeurs pour les réutiliser ultérieurement.

1. >>> t1=(2 == 2)

2. >>> t2=(2 > 3)

3. >>> t1

4. True

5. >>> t2

6. False

7. >>> t1 or t2

8. True

9. >>> t1 and t2

10. False

11. >>> type(t1)

12. <class 'bool'>

13. >>> type(t2)

14. <class 'bool'>

Toutefois, cela reste à la base des valeurs entières valant 1 pour True et 0 pour False. Et selon la façon dont on les emploie, cela peut amener à obtenir finalement des valeurs numériques.

1. >>> True + True

2. 2

3. >>> type(False)

4. <class 'bool'>

5. >>> type(False + False)

6. <class 'int'>

Attention : il convient toutefois de ne pas baser ses algorithmes sur les valeurs numériques de True et False, car elles sont issues de conventions qui peuvent évoluer à tout moment.

La valeur None (de type None) sert à officialiser l'existence d'un élément par une valeur neutre. On peut la comparer au NULL du C. Elle permet de créer une variable ou un container sans avoir besoin de spécifier un type particulier sur son contenu.

1. >>> v=None

2. >>> type(v)

3. <class 'NoneType'>

4. >>> t1=[None, None, None]

5. >>> t2=[None,] * 3

À noter : toute expression, bien que ne valant pas explicitement True ou False est quand même utilisable comme un booléen

- Un int ou un float sont assimilés à True s’ils sont différents de 0 ; sinon ils sont assimilés à False

- Un str, un tuple, un list, un set ou un dict sont assimilés à True s’ils ne sont pas vides (leur longueur est différente de 0) ; sinon ils sont assimilés à False

- None est assimilé à False.

En fait, pour résumer, une expression est toujours considérée comme True sauf si elle est False, None, 0 (et tout ce qui en dérive comme une fraction valant 0 ou un complexe valant 0) ; ou si sa longueur vaut 0.

Attention : ces expressions ne sont que considérées comme des booléens ; sans être toutefois du type bool. Ainsi, comparer une chaîne vide avec False sera faux ; mais utiliser une chaîne vide en tant que False dans un test booléen reste possible.

1. >>> "" == False

2. False

3. >>> "" == True

4. False

5. >>> bool("") == False

6. True

7. >>> print("ok" if not "" else "bad")

8. ok

Plus généralement, on ne compare jamais explicitement avec True ou False, car cela revient à comparer avec 1 ou 0 ce qui n’est pas toujours le résultat attendu par le développeur.

1. >>> print("ok" if 5 == True else "bad")

2. bad

3. >>> print("ok" if 5 is True else "bad")

4. bad

On peut y arriver en passant par la fonction bool() qui, comme pour toutes les autres fonctions de types, force une expression vers son équivalent booléen…

1. >>> print("ok" if bool(5) == True else "bad")

2. ok

… mais c’est encore plus simple de laisser Python effectuer lui‑même l’évaluation booléenne.

1. >>> print("ok" if 5 else "bad")

2. ok

3. >>> print("ok" if not 0 else "bad")

4. ok

IV-9 - Les mots clefs

Les mots clefs de Python sont des mots spécifiques ayant une signification particulière. En tant que tels, ils ne peuvent pas être utilisés par le développeur pour un rôle autre (par exemple comme nom de variable).

Liste des mots clefs Python 3 :
False, None, True, and, as, assert, break, class, continue, def, del, elif, else, except, finally, for, from, global, if, import, in, is, lambda, nonlocal, not, or, pass, raise, return, try, while, with, yield.

Liste des mots clefs Python 2 :
and, as, assert, break, class, continue, def, del, elif, else, except, exec, finally, for, from, global, if, import, in, is, lambda, not, or, pass, print, raise, return, try, while, with, yield.

On peut remarquer que True, False et None ne sont pas des mots clefs dans Python 2. En effet, pour des raisons historiques, ces éléments n’étaient pas à l’origine inclus dans cette version. Ils ont ensuite été rajoutés plus tard, mais sous forme de variables internes à Python.

Ainsi, à l’exception de None protégé par un mécanisme spécifique, rien n’interdit, dans Python 2, de les réutiliser pour autre chose (aux risques du développeur bien évidemment).

1. # Exemple Python 2

2. >>> bool(True) == True

3. True

4. >>> True="Hello"                     # Pourquoi pas…

5. >>> bool(True) == True               # 1 est-il égal à "Hello" ?

6. False                                # Évidemment !!!

8. >>> None="Hello"

9. File "<stdin>", line 1

10. SyntaxError: cannot assign to None

IV-10 - Tout est objet

Python est un langage totalement objet. Et tous les types natifs mentionnés précédemment sont en réalité des types objets et possèdent donc des attributs (variables) et méthodes (actions) utilisables. Attributs et méthodes qui peuvent être visualisés avec la fonction native dir().

1. >>> x=5

2. >>> type(x)

3. <class 'int'>

4. >>> dir(x)

5. ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

6. >>> x.bit_length

7. <built-in method bit_length of int object at 0x93b9890>

8. >>> x.bit_length()

9. 3

De plus, les objets possèdent une mini documentation interne permettant de les décrire et d’expliquer les possibilités les concernant.

Cette documentation est accessible par la fonction help().

1. >>> x=5

2. >>> type(x)

3. <class 'int'>

4. >>> help(x)

5. Help on int object:

7. class int(object)

8. |  int(x=0) -> int or long

9. |  int(x, base=10) -> int or long

10. |

11. |  Convert a number or string to an integer, or return 0 if no arguments

12. |  are given.  If x is floating point, the conversion truncates towards zero.

13. |  If x is outside the integer range, the function returns a long instead.

14. ... (suite de la doc)…

L’aide est aussi disponible pour les méthodes de l’objet.

1. >>> x=5

2. >>> type(x)

3. <class 'int'>

4. >>> help(x.bit_length)

5. Help on built-in function bit_length:

7. bit_length(...)

8. int.bit_length() -> int

10. Number of bits necessary to represent self in binary.

11. >>> bin(37)

12. '0b100101'

13. >>> (37).bit_length()

14. 6

15. (END)

IV-11 - La mutabilité

La mutabilité d’un objet se définit par la possibilité d’effectuer une opération appelée « hash » sur l’objet en question. Il s’agit d’une réduction mathématique représentant un élément de taille théoriquement illimitée (exemple une chaîne) en un nombre fini appelé souvent « somme de contrôle » ou « empreinte numérique ». Si l’opération est possible, on dit alors que l’objet est « hashable ».

Il existe divers algorithmes de hash (sha, md4, md5…). Même une simple action comme associer un fichier et sa taille est considérée comme une opération de hash du fichier.

Cette empreinte étant en quelque sorte une signature numérique de l’objet, en Python, elle doit lui correspondre à tout moment. Cette opération n’est donc possible que si l’objet est immuable.

Les chaînes de caractères et les nombres sont immuables. Des tuples seront immuables qu’à condition qu’ils ne contiennent eux‑mêmes que des objets immuables (si un tuple contient un objet mutable, de façon directe ou indirecte, il n’est pas immuable). Les listes, les ensembles ou les dictionnaires sont des objets naturellement modifiables donc non immuables. True, False et None sont immuables.

La mutabilité d’un objet aura des conséquences sur les façons de le copier (sera vu ultérieurement).

V - Les débuts de la programmation

Bien entendu, on peut utiliser Python pour des tâches plus compliquées que de remplir et d’afficher des variables. Il possède des instructions de contrôle de flux classiques que l’on trouve dans d’autres langages, mais toujours avec ses propres tournures.

V-1 - L’instruction « if »

L’instruction if est sans doute la plus universelle de tous les langages. Elle permet d’effectuer une action (ou un ensemble d’actions) sur une condition vraie et offre la possibilité de rajouter une action (ou un ensemble d’actions) à effectuer dans le cas contraire.

Par exemple :

1. >>> x=int(input("Entrez un nombre: "))

2. Entrez un nombre: 42

3. >>> if x < 0:

4. ...    x=0

5. ...    print("Negatif changé en 0")

6. ... elif x == 0:

7. ...    print("Zero")

8. ... elif x == 1:

9. ...    print("Single")

10. ... else:

11. ...    print("Autre nombre")

12. ...

13. Autre nombre

Les parties elif, qui peuvent être multiples, et la partie else, qui, elle, doit être unique, sont facultatives. Le mot clef elif est un raccourci pour else if, mais permet de gagner un niveau d’indentation. Une séquence if … elif … elif … else est par ailleurs équivalente aux instructions switch+case disponibles dans d’autres langages et qui n’existent pas en Python.

N’oubliez pas l’indentation pour que Python puisse associer les actions à exécuter à l'évaluation de la condition.

V-2 - L’instruction « while »

L’instruction while est la seconde étape dans la programmation Python (ainsi que dans les autres langages structurés). Elle permet d’effectuer une action (ou un ensemble d’actions) répétée tant que la condition exprimée est vraie. Par exemple, ce calcul de la suite de Fibonacci :

1. >>> (a, b)=(0, 1)

2. >>> while b < 1000:

3. ...    print(b, end='')

4. ...    (a, b)=(b, a+b)

5. ...

6. 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987>>>

La boucle while s’exécute tant que la condition (ici b < 1000) reste vraie. La condition peut être n’importe quelle expression représentant un booléen.

Notez aussi l’utilisation de tuples pour regrouper des affectations sur plusieurs variables en une fois.

V-3 - L’instruction « for »

L’instruction for que propose Python est un peu différente de celle que l’on peut trouver dans d'autres langages qui la possèdent. Au lieu de toujours itérer sur une suite arithmétique de nombres comme en Pascal ou de demander à spécifier un début, une condition de continuation et un incrément comme en C ; l’instruction for en Python itère sur les éléments d’une séquence (qui peut être une liste, une chaîne de caractères ou autre itérable…), dans l’ordre dans lequel ils apparaissent dans la séquence. Par exemple :

1. >>> words=("chat", "fenetre", "maison")

2. >>> for w in words:

3. ...    print(w, len(w))

4. ...

5. chat 4

6. fenetre 7

7. maison 6

Alors ça peut paraître assez limitatif comme possibilité, mais comme nous le verrons par la suite, il existe dans Python une telle gamme d’outils et de technologies diverses qui permettent de générer facilement nombreuses séquences et autres itérables qu’en réalité la boucle for est l’une des plus utilisées par les programmeurs.

V-4 - Les instructions « break » et « continue », et les clauses « else » au sein des boucles

Les instructions break et continue sont toujours associées aux boucles. La première interrompt la boucle avant sa fin naturelle tandis que la seconde repart sur l’itération suivante en négligeant toutes les instructions du corps de boucle placées après elle.

Les boucles peuvent également disposer d’une instruction else assez nouvelle par rapport aux autres langages. Celle-ci est exécutée uniquement lorsque la boucle se termine naturellement parce que la condition est devenue fausse (dans le cas d’un while) ou que tous ses éléments ont été traités (dans le cas d’un for) ; mais pas lorsque la boucle est interrompue avant sa fin naturelle par une instruction break.

L’exemple suivant, qui effectue une recherche de nombres premiers, en est une démonstration :

1. >>> n=2

2. >>> while n < 10:

3. ...    x=2

4. ...    while x < n:

5. ...        if (n%x) == 0:

6. ...            print(n, 'égal à ', x, '*', n//x)

7. ...            break

8. ...        x+=1

9. ...    else:

10. ...        print(n, 'est un nombre premier')

11. ...    n+=1

12. ...

13. 2 est un nombre premier

14. 3 est un nombre premier

15. 4 égal à 2 * 2

16. 5 est un nombre premier

17. 6 égal à 3 * 2

18. 7 est un nombre premier

19. 8 égal à 4 * 2

20. 9 égal à 3 * 3

V-5 - L’instruction « pass »

L’instruction pass ne fait rien. Son rôle est juste de fournir une instruction lorsque celle‑ci est nécessaire pour fournir une syntaxe correcte, mais qu’aucune action n’est utile. Par exemple :

1. while True:

2. pass                                # Juste pour attendre le ctrl-c

Un autre cas d’utilisation du pass est de réserver un espace en phase de développement pour une fonction ou un traitement conditionnel, permettant ainsi au développeur de construire son code à un niveau plus abstrait. L’instruction pass est alors ignorée silencieusement :

1. if expression:

2. pass                                # Sera implémenté plus tard

3. else:

4. autre action

V-6 - Les instructions « do…while » et « switch…case »

L’instruction do…while utilisée dans d'autres langages n'existe pas en Python, car sa syntaxe souple et ses outils intégrés permettent de programmer facilement son équivalent.

Équivalent d'un do…while (action et répéter tant que la condition est vraie)…

En C

1. do {

2. action();

3. } while (condition);

En Python

1. while True:

2. action()

3. if not condition: break

L’instruction switch…case elle aussi très souvent utilisée dans d'autres langages n'existait pas jusqu’à Python version 3.10. Jusque là on pouvait programmer plus ou moins facilement son équivalent.

Équivalent d'un switch…case (différentes actions en fonction d'une valeur)…

En C

1. switch (valeur) {

2. case 1: action1(); break;

3. case 2: action2(); break;

4. case 3: action3(); break;

5. default: action_autre();

6. }

En Python

1. {

2. 1: action1,

3. 2: action2,

4. 3: action3,

5. }.get(valeur, action_autre)()

Comme on va le voir, Python a un peu évolué à ce propos.

V-7 - L’instruction « match »

L’instruction match, nouvellement arrivée dans Python version 3.10, a pour but de reprendre l’équivalent de l’instruction switch…case disponible dans beaucoup d’autres langages et qui manquait alors à beaucoup ayant des habitudes avec cette instruction.

Équivalent d'un switch…case sous Python version 3.10 (et supérieurs) …

En C

1. switch (valeur) {

2. case 1: action1(); break;

3. case 2: action2(); break;

4. case 3: action3(); break;

5. default: action_autre();

6. }

En Python

1. match valeur:

2. case 1:

3. action1()

4. case 2:

5. action2()

6. case 3:

7. action3()

8. case _:

9. action_autre()

V-8 - La notion d’expression

Durant tout ce tutoriel a été évoquée à plusieurs reprises la notion d’expression. Il est temps de clarifier un peu ce point.

Une expression en Python est simplement une suite d’opérations représentant une valeur.

Si cette expression est tapée dans l’interpréteur Python sans être récupérée d’une façon ou d’une autre (une variable, une fonction) alors elle est réaffichée immédiatement sur la ligne suivante.

Si cette expression est dans un script là aussi sans être récupérée, alors elle est exécutée silencieusement et disparait.

Une expression peut prendre plusieurs formats :

- la constante simple

1. >>> 123

2. 123

3. >>> "Hello"

4. 'Hello'

- une opération faisant intervenir des constantes ou d’autres opérations

1. >>> 2*2

2. 4

3. >>> "Hello" * 4

4. 'HelloHelloHelloHello'

- une évaluation faisant intervenir des constantes, des opérations ou d’autres évaluations

1. >>> nbr=5

2. >>> nbr, "impair" if (nbr % 2) != 0 else "pair"

3. (5, 'impair')                # Hé oui, la virgule de la ligne précédente crée un tuple !!!

V-9 - Quelques outils

Quelques outils natifs de Python permettant de générer et manipuler des constantes (ou variables ayant pour valeur ces constantes).

- La fonction abs(x) renvoie la valeur absolue de x qui doit être un nombre entier ou à virgule flottante.

- La fonction chr(x) renvoie le caractère correspondant à la valeur unicode (ou ASCII dans Python 2) x.

- La fonction bin(x) renvoie la représentation binaire de l’entier x.

- La fonction cmp(x, y) (Python 2 uniquement) compare les valeurs relatives de x et y et renvoie ‑1, 0 ou 1 selon que x est respectivement plus petit, égal ou plus grand que y.

- La fonction divmod(x, y) renvoie un tuple contenant la division entière et le reste de x par y (concrètement x//y et x%y).

- La fonction eval(s) analyse et évalue l’expression s puis l’exécute en tant qu’instruction Python.

- La fonction exec(s) analyse et évalue l’expression s puis l’exécute en tant que programme Python.

- La fonction hex(x) renvoie la représentation hexadécimale de l’entier x.

- La fonction oct(x) renvoie la représentation octale de l’entier x.

- La fonction ord(x) renvoie la valeur unicode (ou ASCII dans Python 2) du caractère x.

- La fonction round(x, y) renvoie la valeur du flottant x arrondie à y décimales.

VI - la saisie clavier

Dans ce chapitre, on commencera par parler de Python 2, car c’est à partir du comportement de la saisie dans ce langage qu’a été codifiée la saisie dans Python 3.

VI-1 - Python 2

La saisie de valeurs depuis le clavier dans Python 2 peut se faire au travers de deux fonctions :

- input()

- raw_input()

La différence entre les deux fonctions concerne la façon dont elles considèrent les informations récupérées.

Avec la fonction input(), les valeurs récupérées sont typées selon la façon dont l’utilisateur les saisit.

1. >>> while True:

2. ...    x=input("Saisie:")

3. ...    print x, type(x)

4. ...    if not x:

5. ...        print("Terminé")

6. ...        break

7. ...

8. Saisie:5

9. 5, <type 'int'>

10. Saisie:5L

11. 5L, < type 'long'>

12. Saisie:0.5

13. 0.5, < type 'float'>

14. Saisie:"Hello"

15. "Hello", < type 'str'>

16. Saisie:0

17. 0, < type 'int'>

18. Terminé

Avec la fonction raw_input(), les valeurs récupérées sont systématiquement considérées comme des chaînes (str).

1. >>> while True:

2. ...    x=raw_input("Saisie:")

3. ...    print x, type(x)

4. ...    if not x:

5. ...        print("Terminé")

6. ...        break

7. ...

8. Saisie:5

9. '5', < type 'str'>

10. Saisie:5L

11. '5L', < type 'str'>

12. Saisie:0.5

13. '0.5', < type 'str'>

14. Saisie:"Hello"

15. '"Hello"', < type 'str'>

16. Saisie:0

17. '0', < type 'str'>

18. Saisie:

19. '', < type 'str'>

20. Terminé

VI-2 - Python 3

La saisie de valeurs dans Python 3 ne peut se faire qu'au travers de la fonction input(). Celle-ci se comporte comme la fonction raw_input() de Python 2 (les valeurs sont considérées comme des chaînes) qui a été supprimée dans Python 3.

1. >>> while True:

2. ...    x=input("Saisie:")

3. ...    print(x, type(x))

4. ...    if not x:

5. ...        print("Terminé")

6. ...        break

7. ...

8. Saisie:5

9. '5', < class 'str'>

10. Saisie:5L

11. '5L', < class 'str'>

12. Saisie:0.5

13. '0.5', < class 'str'>

14. Saisie:"Hello"

15. '"Hello"', < class 'str'>

16. Saisie:0

17. '0', < class 'str'>

18. Saisie:

19. '', < class 'str'>

20. Terminé

VII - Formatage des chaînes

VII-1 - Le formatage originel

Une chaîne possède un opérateur pourcent (%) permettant de la formater, c'est-à-dire d'y insérer des expressions évaluées à l’exécution.

Le mécanisme ressemble au printf() du C. On écrit la chaîne de base, en préparant la position des expressions à afficher. Cette préparation se fait avec le caractère pourcent (%) suivi de « d » pour indiquer une expression donnant un nombre entier écrit en décimal, « x » pour un nombre hexadécimal, « f » pour un flottant ou plus simplement « s » qui permet de prendre en compte tout le reste (chaînes, listes, tuples, ensembles, dictionnaires, booléens, None) ainsi que les types déjà cités (int, float).

Une fois la chaîne terminée, on y rajoute ensuite de nouveau l’opérateur pourcent (%) pour indiquer qu’elle va se continuer avec des paramètres ; puis on y place derrière soit un tuple contenant toutes les expressions attendues dans l'ordre prévu par la chaîne (si plusieurs éléments attendus) ; soit l'expression attendue si elle est unique.

Exemple :

1. >>> var=5

2. >>> print("Ma variable vaut %d " % var)

3. Ma variable vaut 5

4. >>> print("Ma variable vaut %d et sa moitié vaut %f" % (var, var/2))

5. Ma variable vaut 5 et sa moitié vaut 2.500000

Si tuple il doit y avoir, ce tuple peut bien évidemment parfaitement provenir d’une variable.

1. >>> var=5

2. >>> calc=(var, var /2)

3. >>> print("Ma variable vaut %d et sa moitié vaut %f" % calc)

4. Ma variable vaut 5 et sa moitié vaut 2.500000

Dans le cas de plusieurs expressions mises dans un tuple, l'ordre des expressions du tuple doit correspondre à l'ordre des formats spécifiés dans la chaîne.

On peut toutefois forcer cet ordre en rajoutant des identificateurs entre parenthèses. On associera alors ces identificateurs aux expressions à afficher en remplaçant le tuple final par un dictionnaire avec les identificateurs comme clefs. La chaîne n’aura donc pas besoin de se préoccuper de l’ordre des valeurs attendues puisque le dictionnaire lui donnera un accès direct par l’identificateur.

Exemple :

1. >>> var=5

2. >>> print("Ma variable vaut %(valeur)d et sa moitié vaut %(moitie)f" % {"moitie" : var/2, "valeur" : var})

3. Ma variable vaut 5 et sa moitié vaut 2.500000

Outre la possibilité de ne plus se préoccuper de l'ordre des éléments à afficher, cette technique permet aussi d'afficher plusieurs fois une expression tout en ne l'ayant calculée qu'une seule fois…

1. >>> var=5

2. >>> print("J'ai obtenu le carre %(carre)d en calculant %(valeur)d*%(valeur)d – Ce carre %(carre)d est évidemment un carré parfait de %(valeur)d" % {"valeur" : var, "carre" : var**2})

3. J’ai obtenu le carre 25 calculant 5*5 – Ce carre 25 est évidemment un carré parfait de 5

Les spécificateurs de type peuvent être complétés d’options définissant la façon dont seront affichés ces éléments.

1. >>> "%5s" % "x"             # Justification à droite avec des espaces

2. '    x'

3. >>> "%-5s" % "x"            # Justification à gauche avec des espaces

4. 'x    '

5. >>> "%5d" % 123             # Justification à droite avec des espaces

6. '  123'

7. >>> "%-5d" % 123            # Justification à gauche avec des espaces

8. '123  '

9. >>> "%05d" % 123            # Justification à droite avec des zéros

10. '00123'

11. >>> "%8.2f" % 123           # Justification à droite avec des espaces

12. '  123.00'

13. >>> "%-8.2f" % 123          # Justification à gauche avec des espaces

14. '123.00  '

15. >>> "%08.2f" % 123          # Justification à droite sur 8 digits dont 2 après la virgule avec des 0

16. '00123.00'

Dans le cas où la valeur de justification n’est pas connue, mais calculée dans une expression, il est possible de l’indiquer en remplaçant le nombre par une étoile (*) pour indiquer l’expression qui sera alors placée au même niveau que la valeur à afficher.

1. >>> v=3

2. >>> "%0*d" % (v**2, 123)    # Justification à droite avec des zéros

3. '000000123'

VII-2 - La méthode « format() »

Une chaîne possède aussi une méthode format() permettant des opérations analogues, mais en utilisant des paramètres positionnels. Ces positions se précisent en utilisant les accolades ({}).

Accessoirement, le typage n’est plus obligatoire (plus besoin de spécifier « s », « d » ou autre).

Exemple :

1. >>> var=5

2. >>> print("Ma variable vaut {} et sa moitié vaut {}".format(var, var/2))

3. Ma variable vaut 5 et sa moitié vaut 2.5

De même, cette méthode permet aussi de préciser l’ordre d’affichage des éléments en indiquant cet ordre dans les accolades (le premier élément formaté commençant à 0) et de factoriser les calculs.

1. >>> var=5

2. >>> print("J'ai obtenu le carre {1} en calculant {0}*{0} – Ce carre {1} est évidemment un carré parfait de {0}".format(var, var**2))

3. J’ai obtenu le carre 25 calculant 5*5 – Ce carre 25 est évidemment un carré parfait de 5

De plus, le typage originel ("s", "d", "f", "x") peut quand même être précisé si nécessaire avec le signe deux-points (:)…

1. >>> var=5

2. >>> print("Ma variable vaut {:d} et sa moitié vaut {:f}".format(var, var/2))

3. Ma variable vaut 5 et sa moitié vaut 2.500000

… lesquels peuvent être associés avec l’ordre d’affichage…

1. >>> var=5

2. >>> print("Ma variable vaut {1:d} et sa moitié vaut {0:f}".format(var/2, var))

3. Ma variable vaut 5 et sa moitié vaut 2.500000

… ce qui permet alors d’y rajouter des options définissant la façon dont seront formatés ces éléments.

VII-2-a - Pour formater une chaîne

1. >>> "{:>5s}".format("x")         # Justifier à droite avec des espaces

2. '    x'

3. >>> "{:<5s}".format("x")         # Justifier à gauche avec des espaces

4. 'x    '

5. >>> "{:^5s}".format("x")         # Justifier au milieu avec des espaces

6. '  x  '

7. >>> "{:>05s}".format("x")        # Justifier à droite avec des zéros

8. '0000x'

9. >>> "{:<05s}".format("x")        # Justifier à gauche avec des zéros

10. 'x0000'

11. >>> "{:^05s}".format("x")        # Justifier au milieu avec des zéros

12. '00x00'

VII-2-b - Pour formater un entier

1. >>> "{:>5d}".format(123)         # Justifier à droite avec des espaces

2. '  123'

3. >>> "{:<5d}".format(123)         # Justifier à gauche avec des espaces

4. '123  '

5. >>> "{:^5d}".format(123)         # Justifier au milieu avec des espaces

6. ' 123 '

7. >>> "{:>05d}".format(123)        # Justifier à droite avec des zéros

8. '00123'

9. >>> "{:<05d}".format(123)        # Justifier à gauche avec des zéros

10. '12300'

11. >>> "{:^05d}".format(123)        # Justifier au milieu avec des zéros

12. '01230'

VII-2-c - Pour formater un flottant

1. >>> "{0:>8.2f}".format(123)      # Justifier à droite sur 8 digits dont 2 après la virgule

2. '  123.00'

3. >>> "{:<8.2f}".format(123)       # Justifier à gauche sur 8 digits dont 2 après la virgule

4. '123.00  '

5. >>> "{:^8.2f}".format(123)       # Justifier au milieu sur 8 digits dont 2 après la virgule

6. ' 123.00 '

7. >>> "{:>08.2f}".format(123)      # Justifier à droite avec 0 sur 8 digits dont 2 après la virgule

8. '00123.00'

9. >>> "{:<8.2f}".format(123)       # Justifier à gauche avec 0 sur 8 digits dont 2 après la virgule

10. '123.0000'

11. >>> "{:^8.2f}".format(123)       # Justifier au milieu avec 0 sur 8 digits dont 2 après la virgule

12. '0123.000'

VII-2-d - Remarque

Dans le cas où la valeur de justification n’est pas connue, mais calculée dans une expression, il est possible de spécifier cette expression à travers son propre format positionnel.

1. >>> v=3

2. >>> "{:>0{1}d}".format(123, v**2)        # Justification à droite avec des zéros

3. '000000123'

Pour afficher des accolades littérales dans la chaîne, il faut alors les doubler.

1. >>> var=5

2. >>> print("Ma variable vaut {{{}}} et sa moitié vaut {{{}}}".format(var, var/2))

3. Ma variable vaut {5} et sa moitié vaut {2.5}

À noter que la méthode format() était destinée à devenir le standard unique Python 3. Toutefois, là encore, devant les demandes de la communauté, l’opérateur pourcent de Python 2 y a aussi été laissé.

VII-3 - La méthode « f-strings »

Depuis Python 3.6, les « f-strings » permettent d’insérer des expressions avec ou sans mise en forme dans des chaînes de caractères en utilisant une syntaxe minimale.

Pour utiliser f-strings, il suffit de mettre un « f » devant la chaîne de caractères et pour insérer la valeur d’une variable ou d’une expression dans la chaîne, il suffit de mettre la variable ou l’expression entre accolades sans se préoccuper des guillemets qui encadrent la chaîne.

Exemple :

1. >>> var=5

2. >>> print(f"Ma variable vaut {var} et sa moitié vaut {var/2}")

3. Ma variable vaut 5 et sa moitié vaut 2.5

Comme pour la méthode format() le typage habituel (« s », « d », « f », « x ») peut quand même être précisé si nécessaire avec les deux-points (:)…

1. >>> var=5

2. >>> print(f"Ma variable vaut {var:d} et sa moitié vaut {var/2:f}")

3. Ma variable vaut 5 et sa moitié vaut 2.500000

… ce qui permet alors d’y rajouter des options définissant la façon dont seront formatés ces éléments.

VII-3-a - Pour formater une chaîne de caractères

1. >>> var="x"

2. >>> f"{var:>5s}"                # Justifier à droite avec des espaces

3. '    x'

4. >>> f"{var:<5s}"                # Justifier à gauche avec des espaces

5. 'x    '

6. >>> f"{var:^5s}"                # Justifier au milieu avec des espaces

7. '  x  '

8. >>> f"{var:>05s}"               # Justifier à droite avec des zéros

9. '0000x'

10. >>> f"{var:<05s}"               # Justifier à gauche avec des zéros

11. 'x0000'

12. >>> f"{var:^05s}"               # Justifier au milieu avec des zéros

13. '00x00'

VII-3-b - Pour formater un entier

1. >>> var=123

2. >>> f"{var:>5d}"                # Justifier à droite avec des espaces

3. '  123'

4. >>> f"{var:<5d}"                # Justifier à gauche avec des espaces

5. '123  '

6. >>> f"{var:^5d}"                # Justifier au milieu avec des espaces

7. ' 123 '

8. >>> f"{var:>05d}"               # Justifier à droite avec des zéros

9. '00123'

10. >>> f"{var:<05d}"               # Justifier à gauche avec des zéros

11. '12300'

12. >>> f"{var:^05d}"               # Justifier au milieu avec des zéros

13. '01230'

VII-3-c - Pour formater un nombre à virgule flottante

1. >>> var=123.00

2. >>> f"{var:>8.2f}"              # Justifier à droite sur 8 digits dont 2 après la virgule

3. '  123.00'

4. >>> f"{var:<8.2f}"              # Justifier à gauche sur 8 digits dont 2 après la virgule

5. '123.00  '

6. >>> f"{var:^8.2f}"              # Justifier au milieu sur 8 digits dont 2 après la virgule

7. ' 123.00 '

8. >>> f"{var:>08.2f}"             # Justifier à droite avec 0 sur 8 digits dont 2 après la virgule

9. '00123.00'

10. >>> f"{var:<08.2f}"             # Justifier à gauche avec 0 sur 8 digits dont 2 après la virgule

11. '123.0000'

12. >>> f"{var:^08.2f}"             # Justifier au milieu avec 0 sur 8 digits dont 2 après la virgule

13. '0123.000'

VII-3-d - Remarque

Dans le cas où la valeur de justification n’est pas connue, mais calculée dans une expression, il est possible de rajouter des accolades internes pour spécifier cette expression.

1. >>> var=123

2. >>> n=3

3. >>> f"{var:>0{n**2}d}"          # Justification à droite avec des zéros

4. '000000123'

VIII - L’encodage d’une chaîne

L’encodage est le nom donné à la façon de coder les caractères en binaire. Jusqu’à il y a une dizaine d’années, l’encoding habituel était le standard ASCII (nom officiel « latin‑1 ») qui utilise 7 bits et permet donc de coder 128 caractères (l’ASCII étendu utilise 8 bits et monte alors jusqu'à 256). Avec la popularisation de l’Internet et ses échanges d’informations entre différents pays, cet encoding s’est vite avéré insuffisant pour pouvoir coder tous les caractères des différents alphabets mondiaux (le Kanji, alphabet japonais, en contient par exemple plus de 6 000). Il a alors évolué en un codage plus général appelé Unicode. Et l’Unicode a dérivé vers l’utf qui est en fait un mélange ASCII/Unicode qui se résume, de façon simpliste, en « utiliser l’ASCII quand c’est possible sinon passer en Unicode ».

Python permet de créer une chaîne selon différents encodings. Cependant, les encodages par défaut ont changé entre Python 2 et Python 3.

VIII-1 - La chaîne de base

La chaîne de base s’écrit de façon naturelle (encadrée de quotes simples ou doubles comme dans tous les exemples de chaînes vus jusqu’à présent). Dans Python 2, cette chaîne est encodée en ASCII. Dans Python 3, cette chaîne est encodée en Unicode. Dans les deux versions de Python, son type est nommé « str ».

1. chaine="chaine de base"

VIII-2 - Le préfixe « u »

Une chaîne préfixée par un caractère « u » (Unicode) est encodée en Unicode dans les deux Python. Dans Python 2, son type est nommé « Unicode » tandis que dans Python 3, son type est nommé « str ».

1. chaine=u"chaine en Unicode"

VIII-3 - Le préfixe « b »

Une chaîne préfixée par un caractère « b » (bytes) est encodée en ASCII dans les deux versions de Python. Dans Python 2, son type est nommé « str » tandis que dans Python 3, son type est nommé « bytes ».

1. chaine=b"chaine en ASCII"

VIII-4 - Le préfixe « r »

Une chaîne préfixée par un caractère « r » (raw) correspond à une chaîne de base (comme vu au début) toutefois, les caractères spéciaux (comme le backslash) ne sont pas interprétés comme caractères spéciaux, mais comme de simples caractères normaux.

1. chaine1="Ceci est une phrase\ncontenant deux lignes…"

2. chaine2=r"Ceci est un simple texte\nsur une seule ligne…"

VIII-5 - Récapitulatifs

Pour résumer, une chaîne préfixée par « u » est toujours encodée en Unicode, aussi bien en Python 2 qu’en Python 3 ; et une chaîne préfixée par « b » est toujours encodée en ASCII, aussi bien en Python 2 qu’en Python 3. Mais dans ces deux cas, le nom du type n’est pas le même entre Python 2 (« Unicode ») et Python 3 (« str »).

Et une chaîne qui n’est pas préfixée, ou bien préfixée par « r », a le même nom de type (« str ») aussi bien en Python 2 qu’en Python 3 ; mais ce type « str » n’a pas le même encodage entre Python 2 (ASCII) et Python 3 (Unicode).

Syntaxe

Python 2

Python 3

Type

Encoding

Type

Encoding

chaine="bonjour"
chaine=r"bonjour"

str

ASCII

str

Unicode

chaine=u"bonjour"

Unicode

Unicode

str

Unicode

chaine=b"bonjour"

str

ASCII

bytes

ASCII

Le changement d’encodage des chaînes standards entre Python 2 et Python 3 est un des plus gros soucis de portabilité des scripts. Le développeur qui veut partir sur un code Python 2, mais qui prévoit de passer à moyenne échéance sur Python 3 a tout intérêt à utiliser dès le départ l’Unicode en préfixant systématiquement ses chaînes avec un « u ».

De plus, un autre souci peut provenir de toute communication entrante ou sortante avec des outils qui n'utilisent pas l'Unicode. Dans ce cas, il est préconisé de convertir les chaînes en provenance de l'outil en Unicode le plus tôt possible ; de travailler en interne Python en Unicode ; et de convertir les chaînes devant sortir vers l'outil dans l'encoding de l'outil le plus tard possible.

Attention : il ne faut pas confondre l’encoding du script, qui est le codage dans lequel le script est écrit, et qui est précisé avec la seconde ligne # coding: xxx ; et les chaînes manipulées dans le script. On peut très bien avoir un script écrit en ASCII et travaillant avec des chaînes Unicode.

IX - Les fonctions

IX-1 - Généralités

Une fonction est un mécanisme permettant d’encapsuler un travail spécifique dans un bloc particulier identifié par un nom logique (nom de la fonction). Offrant ainsi à l’utilisateur la possibilité d'invoquer le bloc par son nom pour faire exécuter le travail encapsulé au moment de son choix.

Exemple : créer une fonction nommée salut qui affiche Hello

1. >>> def salut():

2. ...    print("Hello")

3. ...

4. >>> salut()                # Appel de la fonction (les parenthèses entrainent l'action)

5. Hello

L'instruction def introduit une définition de fonction. Elle doit être suivie du nom de la fonction et de deux parenthèses ainsi que du fameux caractère « deux-points » (:) terminant systématiquement l’instruction définissant le bloc.

N’oubliez pas ensuite qu’il s’agit d’un bloc, donc les instructions de la fonction doivent être indentées pour bien indiquer leur appartenance à la fonction.

En dehors de son nom, deux autres catégories d'éléments caractérisent une fonction :

- les diverses variables permettant de récupérer des informations envoyées par l’appelant et qui seront utilisées ensuite dans la fonction pour paramétrer son travail. Elles sont placées dans les parenthèses de la fonction et sont nommées les « paramètres » de la fonction. Et du côté appelant, les valeurs envoyées sont nommées « arguments ». Dans la théorie des langages, le nombre de paramètres et leurs types sont appelés « signature » de la fonction. Comme en Python le type d’une variable est défini à sa création, il n’existe pas donc pas de type spécifique pour un paramètre. La signature d’une fonction est donc simplement son nombre de paramètres ;

- la valeur finale qu’elle produit (résultat) et qu’elle retourne à l’appelant qui l’attend pour continuer son travail. Elle sera effectivement renvoyée grâce à l’instruction return.

Exemple : une fonction qui additionne ses deux arguments reçus

1. >>> def addition(a, b):

2. return a+b

3. ...

4. >>> calc=addition(2, 3)        # Appel de la fonction et récupération de son résultat

5. >>> print(calc)                # Affichage du résultat

6. 5

7. >>> calc=2 * addition(4, 5)    # Utilisation du résultat de la fonction dans un calcul

8. >>> print(calc)                # Affichage du résultat du calcul

9. 18

10. >>> print(addition(6, 7))      # Affichage direct du résultat de la fonction

11. 13

L’ordre des paramètres dans la fonction est important, car il conditionne l’ordre des arguments à passer lors de l’appel. Le premier argument ira dans le premier paramètre, le second argument ira dans le second paramètre, etc. On les nomme des « paramètres positionnels ».

Contrairement à d'autres langages, Python ne fait aucune différence entre « fonction » (qui renvoie une valeur) et « procédure » (qui ne renvoie rien). D'ailleurs, toute fonction renvoie toujours une valeur qui est par défaut None si elle se termine sans avoir renvoyé autre chose.

Plusieurs return sont possibles au sein d’une fonction. C’est le premier rencontré lors de l’exécution qui sera effectif. Cela permet de rendre une fonction plus modulable ou adaptative aux circonstances. Néanmoins, certains puristes préfèrent qu’il n’y ait qu’un seul return par fonction, quitte à adapter (voire parfois alourdir) leur code pour arriver à ce résultat.

IX-2 - Valeur par défaut des paramètres

Il est possible de proposer des valeurs par défaut pour des paramètres qui permettront alors à l’appelant d’omettre certains arguments.

Cela se fait avec l’opérateur « égal » qu’on rajoute au paramètre. Par exemple :

1. def ask_ok(prompt, essai=1, echec="Oui ou Non SVP !!!"):

2. while True:

3. ok=input(prompt)

4. if ok in ("y", "yep","yeah", "yes"): return True

5. if ok in ("n", "no", "nop", "nope"): return False

6. print(echec)

7. if essai <= 1: return None

8. essai-=1

Cette fonction doit être appelée avec impérativement un argument qui sera stocké dans le paramètre prompt. Les autres paramètres essai et echec peuvent être omis et dans ce cas, ils prendront la valeur spécifiée dans la définition de la fonction.

Exemple :

- ask_ok("Voulez-vous vraiment quitter ? ")

- ask_ok("OK pour écraser le fichier ?", 2)

- ask_ok("OK pour écraser le fichier ? ", 2, "Allez, seulement oui ou non !")

Attention : les paramètres ayant une valeur par défaut doivent être placés après les paramètres n’en ayant pas, car Python les remplira selon les positions des arguments passés lors de l’appel. De plus, si on omet un paramètre en lui faisant utiliser sa valeur par défaut, on est obligé d’omettre aussi tous ceux placés après, sauf si on les nomme explicitement (ce sera vu ultérieurement). Il est donc, par exemple, impossible d’appeler cette fonction en fournissant une valeur particulière pour le paramètre echec sans en fournir aussi une pour le paramètre essai.

De plus, la valeur par défaut d’un paramètre n’est créée en mémoire qu’une seule fois au moment où la fonction est créée et non au moment où elle est appelée :

1. >>> i=1

2. def fct(j=i): print(j)

3. ...

4. >>> fct()

5. 1

6. >>> i=2

7. >>> fct()

8. 1

9. >>>

Cela peut alors devenir problématique lorsque cette valeur par défaut est un objet mutable tel qu’une liste, un dictionnaire ou des instances de la plupart des classes. Par exemple, la fonction suivante accumule les arguments qui lui sont passés au fil des appels successifs :

1. >>> def f(n, tab=[]):

2. ...    tab.append(n)

3. ...    return tab

4. ...

5. >>> f(1)

6. [1]

7. >>> f(2)

8. [1, 2]

9. >>> f(3)

10. [1, 2, 3]

Pour corriger ce souci, il faut alors utiliser une valeur immuable comme valeur par défaut (la plus neutre et faite pour ça étant la valeur None) ; et ensuite, tester cette valeur. Exemple :

1. >>> def f(n, tab=None):

2. ...    if tab is None: tab=[]

3. ...    tab.append(n)

4. ...    return tab

5. ...

6. >>> f(1)

7. [1]

8. >>> f(2)

9. [2]

10. >>> f(3)

11. [3]

IX-3 - Les paramètres nommés

Les fonctions peuvent également être appelées en spécifiant explicitement dans quel paramètre doit aller tel argument, sous la forme parametre=valeur. Dans ce cas, la position des arguments lors de l’appel n’a plus d’importance puisqu’on associe explicitement un argument à son paramètre.

La combinaison positionnel/nommé est possible à condition que les arguments nommés soient placés après les arguments positionnels.

Et, quelle que soit la façon d’appeler, il faut dans tous les cas veiller à ce que chaque paramètre ne soit rempli qu’avec un seul argument.

Par exemple, une fonction qui indique si p est divisible par q :

1. def divisible(p, q=1): return (p%q) == 0

Cette fonction accepte un paramètre obligatoire (p) et un paramètre facultatif (q). Elle peut alors être appelée de n’importe laquelle des façons suivantes…

1. divisible(5, 2)              # deux arguments positionnels

2. divisible(5, q=2)            # un argument positionnel, un argument nommé placé après

3. divisible(5)                 # un argument positionnel, un argument par défaut

4. divisible(p=5)               # un argument nommé, un argument par défaut

5. divisible(p=5, q=2)          # deux arguments nommé

6. divisible(q=2, p=5)          # deux arguments nommés (l’ordre n’a alors pas d’importance)

… mais tous les appels qui suivent sont incorrects :

1. divisible()                  # un argument obligatoire manquant

2. divisible(p=5, 2)            # un argument positionnel après un argument nommé

3. divisible(5, p=2)            # deux arguments pour le même paramètre "p"

IX-4 - Verrouillage des méthodes de passage des arguments

Comme on vient de le voir, une fonction peut être appelée avec des paramètres positionnels ou des paramètres nommés au choix de l'appelant.

Cependant, depuis Python 3, le concepteur d'une fonction peut imposer à l'appelant de lui passer ses paramètres exclusivement par nom.

Cela se fait en positionnant le caractère étoile (*) dans la liste des paramètres de la fonction. Tout paramètre placé après l'étoile sera impérativement nommé, et tout paramètre placé avant restera au choix de l'appelant.

Exemple :

1. >>> def fct(a, *, b): print(a, b)

Cette fonction laisse le choix libre pour le paramètre a et impose que le paramètre b soit explicitement nommé lors de l'appel. Elle peut alors être appelée de n’importe laquelle des deux façons suivantes…

1. >>> fct(1, b=2)

2. 1 2

3. >>> fct(a=1, b=2)

4. 1 2

… mais l’appel suivant est incorrect :

1. >>> fct(1, 2)                # Le 2° argument n'est pas nommé

En plus de cette première possibilité, il a été rajouté dans Python 3.8 une seconde syntaxe pour imposer aussi un passage par position.

Cela se fait en positionnant le caractère slash (/) tout en laissant la possibilité de placer aussi le caractère étoile (*) dans la liste des paramètres de la fonction. Tout paramètre placé avant le slash sera impérativement positionnel, tout paramètre placé après l'étoile sera impérativement nommé, et tout paramètre placé entre les deux (ou après le premier ou avant le second s'il n'y en a qu'un seul) restera au choix de l'appelant.

Exemple :

1. >>> def fct(a, /, b, *, c): print(a, b, c)

Cette fonction demande que son premier paramètre a soit donné par position, laisse le choix libre pour le paramètre b et impose que le paramètre c soit explicitement nommé lors de l'appel. Elle peut alors être appelée de n’importe laquelle des deux façons suivantes…

1. >>> fct(1, 2, c=3)

2. 1 2 3

3. >>> fct(1, b=2, c=3)

4. 1 2 3

… mais les deux appels qui suivent sont incorrects :

1. >>> fct(a=1, b=2, c=3)       # Le 1er argument n'est pas positionnel

2. >>> fct(1, 2, 3)             # Le 3° argument n'est pas nommé

Bien évidemment, le concepteur de la fonction peut, à sa discrétion, ne verrouiller qu'une des deux formes d'appel (en n'utilisant que le slash ou que l'étoile), ou aucune.

IX-5 - Les annotations

Les annotations déjà vues pour les variables de base sont aussi possibles pour les paramètres (qui ne sont eux aussi que des variables) avec les mêmes règles et la même syntaxe.

De plus, l’annotation est aussi possible pour la fonction permettant alors d’indiquer le type de valeur renvoyée. Cela se fait en rajoutant ‑> nom_du_type_renvoyé avant les deux‑points terminant la fonction.

Exemple :

Syntaxe habituelle

1. def divisible(a, b=1):

2. return (a % b) == 0

Nouvelle syntaxe possible

1. def divisible(a:int, b:int=1) ‑> bool:

2. return (a % b) == 0

Comme pour les variables, cette notation est facultative, mais si elle est appliquée, le typage indiqué doit correspondre à la même liste int, float, bytes, str, bool, tuple, list, set, dict, None, callable, object et any ; plus les éventuels noms des objets personnels (sera vu ultérieurement) créés en amont dans le code.

Et comme pour les variables, ces indications sont, pour l’instant, purement informatives et non contrôlées.

IX-6 - Les paramètres supplémentaires facultatifs

Après les paramètres classiques, on peut éventuellement rajouter deux paramètres très particuliers :

- *args : tuple qui contiendra tous les arguments positionnels supplémentaires (donc autres que ceux déjà prévus) passés à la fonction ;

- **kwargs : dictionnaire qui contiendra tous les arguments nommés supplémentaires passés à la fonction. (les clefs du dictionnaire seront les noms des paramètres et les valeurs du dictionnaire seront les valeurs des arguments).

Les noms args et kwargs sont libres et peuvent être remplacés par n'importe quel autre nom de variable valable, mais ceux‑ci correspondent à des conventions entre programmeurs Python.

Les deux sont facultatifs, mais si **kwargs est présent, il doit alors être placé en dernier. Et si *args est aussi présent, sous‑entendu qu'il doit toujours être placé avant **kwargs, il peut alors être placé à n’importe quelle position dans Python 3 (tous les paramètres situés après devront alors être impérativement passés sous forme nominale pour que Python puisse s'y retrouver) et exclusivement en dernière position disponible dans Python 2.

1. >>> def etat_civil(nom, *args, grade, **kwargs):

2. ...    print("nom=", nom, type(nom))

3. ...    print("args=", args, type(args))

4. ...    print("grade=", grade, type(grade))

5. ...    print("kwargs=", kwargs, type(kwargs))

6. ...

7. >>> etat_civil("Cesar", "Julius", "Caius", grade="General", victoire="Alesia", defaite="Gergovie")

8. nom= Cesar <class 'str'>

9. args= ('Julius', 'Caius') <class 'tuple'>

10. grade= General <class 'str'>

11. kwargs= {'victoire': 'Alesia', 'defaite': 'Gergovie'} <class 'dict'>

Dans cet exemple, le paramètre nom (le premier) reçoit le premier argument passé lors de l’appel ("Cesar"). Le tuple args reçoit les autres arguments positionnels qui ne sont pas explicitement affectés ("Julius" et "Caius"), le paramètre grade reçoit la valeur demandée explicitement par l'appelant ("General") et le dictionnaire kwargs reçoit lui les arguments nommés non récupérés par un paramètre adéquat ("Alesia" et "Gergovie") avec les clefs qui sont prises dans les noms d’affectation ("victoire" et "defaite").

IX-7 - Utilisation inverse

La situation inverse peut survenir lorsque les valeurs à transmettre sont déjà dans une liste, un tuple ou un dictionnaire ; alors que la fonction attend des paramètres bien distincts.

On peut alors envoyer directement la liste ou le tuple vers la fonction en le faisant précéder d’une étoile ; et le dictionnaire en le faisant précéder de deux étoiles. Pour ce dernier, ses clefs doivent correspondre aux noms des paramètres attendus par la fonction.

On nomme cette opération « unpacking ».

1. >>> def hypotenuse(a, b): return (a ** 2 + b ** 2) ** 0.5

2. ...

3. >>> tpl=(3, 4)

4. >>> dico={"a" : 3, "b" : 4}                    # Clefs identiques aux paramètres

5. >>>

6. >>> hypotenuse(tpl[0], tpl[1]))                # Appel classique

7. 5.0

8. >>> hypotenuse(dico["a"], dico["b"]))          # Appel classique

9. 5.0

10. >>>

11. >>> hypotenuse(*tpl)                           # Appel par unpacking du tuple

12. 5.0

13. >>> hypotenuse(**dico)                         # Appel par unpacking du dictionnaire

14. 5.0

IX-8 - Détails de syntaxe

Comme cela a été vu, l’instruction définissant une fonction commence par le mot clef def suivi de son nom, puis de parenthèses contenant les arguments.

1. def add(a, b=0):

2. return a+b

Pour plus de lisibilité, il est possible de mettre ces arguments sur chaque ligne terminée par une virgule.

1. def mult(

2. a,

3. b=1,

4. ):

5. return a*b

Spécificité Python 2 : si le dernier argument est *args ou **kwargs, ils ne doivent pas se terminer par une virgule. Cette contrainte n’existe plus dans Python 3 qui accepte maintenant les deux syntaxes (avec ou sans virgule).

IX-9 - Fonctions anonymes

Le mot clef lambda permet de créer de petites fonctions anonymes.

Exemple :

1. >>> hypotenuse=lambda a, b: (a ** 2 + b ** 2) ** 0.5

Les fonctions lambda sont syntaxiquement restreintes à une seule expression qui équivaut au return et peuvent être utilisées partout où un objet fonction est attendu. Elles ne sont qu’un sucre syntaxique remplaçant une définition de fonction à une instruction. Généralement, un usage typique est de passer directement une fonction anonyme en tant qu’argument lorsqu’un outil particulier attend une fonction.

1. >>> nbr=[(1, 'un'), (2, 'deux'), (3, 'trois'), (4, 'quatre')]

2. >>>

3. >>> # Tableau trié sur chaque x[1], x itérant chaque tuple de nbr, x[1] étant donc la chaîne associée

4. >>> sorted(nbr, key=lambda x: x[1])                # Tri du tableau – La clef est une fonction lambda

5. [(2, 'deux'), (4, 'quatre'), (3, 'trois'), (1, 'un')]

Mais, inversement, il est déconseillé de passer par des fonctions lambda pour s’éviter le travail d’avoir à définir une fonction de façon explicite.

Mauvaise pratique

1. lambda sqrt n: n ** 0.5

Bonne pratique

1. def sqrt(n): return n ** 0.5

IX-10 - Les fonctions incluses

Une fonction peut aussi s’inclure dans une autre fonction. Dans ce cas, elle ne sera visible et utilisable que depuis la fonction dans laquelle elle a été incluse.

1. >>> def a():

2. ...    def xxx():

3. ...        print("xxx")

4. ...    xxx()

5. ...    print("a")

7. >>> a()

8. xxx

9. a

10. >>> xxx()

11. Traceback (most recent call last):

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

13. NameError: name 'xxx' is not defined

Dès lors, deux fonctions incluses dans deux fonctions distinctes peuvent avoir le même nom sans que cela ne gêne.

1. >>> def a():

2. ...    def xxx():

3. ...        print("xxxA")

4. ...    xxx()

5. ...    print("a")

7. >>> def b():

8. ...    def xxx():

9. ...        print("xxxB")

10. ...    xxx()

11. ...    print("b")

13. >>> a()

14. xxxA

15. a

16. >>> b()

17. xxxB

18. b

Attention : les fonctions incluses sont recréées à chaque appel de la fonction qui les contient ce qui peut nuire aux performances.

IX-11 - La récursivité

IX-11-a - Le concept

La récursivité est la possibilité offerte à une fonction de s’appeler elle-même ; et à attendre le retour de ce sous-appel pour continuer son propre travail.

L’exemple le plus classique de la récursivité est le calcul de la factorielle d’un nombre « n » (factorielle qui se note « n! ») et qui se calcule de la façon suivante : n!=1*2*3*…*(n-1)*n ; avec la convention que 0!=1.

Algorithmiquement, on se rend vite compte que n!=n*(n-1)! ; ce qui permet de définir la fonction de la façon suivante :

1. def factorielle(n):

2. if n == 0: return 1

3. return n * factorielle(n-1)

Le principe de l’écriture d’une fonction récursive est généralement le suivant :

- écriture du cas entrainant l’arrêt de la récursivité (impératif sinon la fonction n’a aucun repère pour savoir quand arrêter de s’appeler),

- appel de la fonction avec un argument différent de l’argument reçu (impératif sinon le nouvel appel étant identique à l’ancien, la fonction n’a aucun moyen de le différencier).

Et les possibilités syntaxiques de Python permettent de regrouper ces règles de façon plus succinctes.

1. def factorielle(n): return n * factorielle(n-1) if n else 1

IX-11-b - Les inconvénients

Toutefois, si la récursivité amène une simplicité d’écriture de code, elle entraine en retour une surcharge non négligeable pour le moteur Python qui doit, pour invoquer une fonction récursive, commencer par sauvegarder le contexte actuel avant d’en instancier un nouveau. De plus, la profondeur de récursion est limitée (1000 par défaut).

Cette surcharge devient exponentielle dans le cas de récursivité multiple (la fonction mère appelant plusieurs fois la fonction fille) comme dans le cas de la suite de Fibonacci (U2=U0+U1).

1. def fibonacci(n):

2. if n < 2: return (1, 1)[n]

3. return fibonacci(n-2) + fibonacci(n-1)

Les appels n’étant pas mémorisés, ceux-ci se font et se refont plusieurs fois inutilement. Exemple : pour U30 il faudra calculer U29 et U28. Pour U29 il faudra calculer U28 (déjà calculé) et calculer U27. Pour U28 (donc calculé 2 fois) il faudra calculer U27 (déjà calculé) et calculer U26. Etc.

C’est pourquoi il est conseillé de réfléchir si un algorithme non récursif (nommé « itératif ») ne serait pas préférable si celui‑ci est possible.

1. def factorielle(n):

2. res=1

3. while n > 1:

4. res*=n

5. n-=1

6. return res

8. def fibonacci(n):

9. res=(1, 1)

10. if n < 2: return res[n]

11. while n > 1:

12. res=(res[1], res[0] + res[1])

13. n-=1

14. return res[-1]

Si l’algorithme itératif n’est pas possible, il peut-être alors avantageux de simuler la récursivité en utilisant une liste qui servira à empiler et dépiler les appels et résultats.

1. def factorielle(n):

2. res=1

3. pile=[n,]

4. while pile:

5. v=pile.pop()

6. if v > 1:

7. res*=v

8. pile.append(v-1)

9. return res

11. def fibonacci(n):

12. base=(1, 1)

13. res=0

14. pile=[n,]

15. while pile:

16. v=pile.pop()

17. if v > 1:

18. pile.extend((v-1, v-2))

19. else:

20. res+=base[v]

21. return res

Dans ce cas, cela ne change rien aux coûts de calcul (pour le calcul de par exemple U30, U27 sera alors empilé et dépilé 3 fois) mais ça évite les soucis liés à la sauvegarde/restitution du contexte de travail ainsi que la limite de la récursion.

IX-11-c - Optimisation

En dehors de sa charge pour le système, un autre inconvénient souvent rencontré dans la mise en place d'une fonction récursive, ce sont les tests préalables et répétés alors de façon récursive.

Reprenons l'exemple de la factorielle en lui faisant vérifier que le paramètre reçu est bien positif :

1. def factorielle(n):

2. if n < 0: return 0

3. if n == 0: return 1

4. return n * factorielle(n-1)

Il est évident que cette vérification n'a de sens que pour le tout premier appel, celui qui est fait par l'utilisateur de la fonction ; et que les appels récursifs effectués par la fonction elle‑même se feront avec un argument correct. Malheureusement ,telle qu'est programmée la fonction, chaque appel récursif effectuera cet inutile contrôle.

Généralement, pour éviter ce souci connu, le développeur programme alors deux fonctions distinctes : la première dédiée aux utilisateurs et se chargeant des contrôles nécessaires, et la seconde dédiée aux calculs et effectuant le travail récursif. Mais cette dernière reste quand même accessible et utilisable par quiconque connait son existence.

Python permet de résoudre cet inconvénient au travers de l'inclusion de fonctions. Le développeur pourra alors encapsuler la fonction récursive dans une couche principale non récursive qui, elle, se chargera des contrôles préalables. Mais la fonction récursive interne ne sera pas accessible depuis l'extérieur.

1. # Création de la fonction factorielle de façon récursive

2. def facto(n):

3. # La sous-fonction moteur du calcul récursif

4. def calcul_r(n):

5. if n == 0: return 1

6. return n * calcul_r(n-1)

8. # Corps de la fonction principale – Test du paramètre reçu

9. if n < 0: return 0

11. # Ici tout est ok - Renvoi calcul récursif de la factorielle

12. return calcul_r(n)

X - Les iterables

X-1 - La notion d’« itérable »

Un itérable est un ensemble de valeurs groupées et pouvant subir une « itération », c’est-à-dire être traité par une boucle qui récupèrera les valeurs une à une.

En effet, dans les programmes usuels, l’immense majorité des opérations se résume généralement à prendre un ensemble d’éléments et les traiter un à un (traiter les clients d’une banque, les comptes bancaires d’un client, les lignes d’opérations dans un compte, les salles de classe d’un lycée, les cours dans une salle, les élèves dans un cours, les fichiers d’un dossier, les lignes d’un fichier, les mots d’une ligne, etc.).

Python a donc été conçu spécifiquement pour offrir des fonctions de manipulation et de gestion de tout ce qui est itérable. Il essaye de rendre itérable tout ce qui semble l’être naturellement (les éléments d’une liste, d’un tuple, d’un dictionnaire, les lettres d’une chaîne, les lignes d’un fichier, etc.) afin d’offrir au programmeur une possibilité accrue d’automatiser facilement ses algorithmes.

Un programmeur Python tentera alors, lui aussi assez naturellement, de favoriser au maximum les itérations dans ses codes.

Exemple : détecter si une personne mesure plus de 175cm et pèse plus de 80kg.

Écriture algorithmique classique :

1. if taille > 175 and poids > 80:

2. print("trouvé")

Le même programme utilisant une itération sur les critères transformés en itérables :

1. for (item, limite) in ((taille, 175), (poids, 80)):

2. if item <= limite : break

3. else:

4. print("trouvé")

Dans l'écriture classique, le rajout d’une condition (age > 18 par exemple) se traduira par une réécriture de la condition (rajout d’un and, etc.). Dans la seconde version, cela se traduit juste par un tuple de plus dans l’itérable qui peut être aussi écrit de façon plus lisible.

1. for (item, limite) in (

2. (taille, 175),

3. (poids, 80),

4. (age, 18),

5. ):

6. if item <= limite : break

7. else:

8. print("trouvé")

Ce qui permet aussi de désactiver/réactiver facilement certaines conditions dans des phases de test.

1. for (item, limite) in (

2. #(taille, 175),              # Cette ligne est désactivée par le dièse

3. #(poids, 80),                # Cette ligne est désactivée par le dièse

4. (age, 18),

5. ):

6. if item <= limite : break

7. else:

8. print("trouvé")

X-2 - Les outils Python

Quelques outils natifs Python permettant de générer et manipuler des itérables

La fonction range(*args) produit une liste de nombres entre :

- 0 et args[0] (exclu) par pas de 1 si la fonction est appelée avec un seul argument

- args[0] (inclus) et args[1] (exclu) par pas de 1 si la fonction est appelée avec deux arguments

- args[0] (inclus) et args[1] (exclu) par pas de args[2] si la fonction est appelée avec trois arguments

Elle permet de créer facilement les indices d'un tuple ou d'une liste.

Dans Python 3, il s’agit d’un générateur (sera vu ultérieurement) tandis que dans Python 2 cette fonction renvoie directement la liste des nombres générés.

La fonction xrange(*args) (Python 2 uniquement) est l’ancien nom de ce qui est devenu range() dans Python 3. Elle produit sa liste de nombres à travers un générateur (sera vu ultérieurement).

1. >>> enfants=("Pim", "Pam", "Poum")

2. >>> for i in range(len(enfants)): print(i+1, enfants[i])

3. ...

4. 1, Pim

5. 2, Pam,

6. 3, Poum

7. >>> for i in range(len(enfants), 0, -1): print(i, enfants[i-1])

8. ...

9. 3, Poum

10. 2, Pam,

11. 1, Pim

À noter donc que la fonction range() de Python 2 a disparu dans Python 3 et seule reste la fonction passant par un générateur.

La fonction iter(iterable) crée un itérateur manipulable à partir de l'itérable ou renvoie l'exception TypeError si l'itérable n'en est pas un.

L'accès à l'itérateur se fait ensuite par l'intermédiaire de la fonction next(iterateur[, default]) qui retourne à chaque appel l'élément suivant de l'itérable ; ou lève une exception StopIteration quand il n'y en a plus à moins qu’une valeur par défaut ait été demandée. Auquel cas la fonction retournera indéfiniment cette valeur à chaque appel.

1. >>> enfants=iter(("Pim", "Pam", "Poum"))

2. >>> while True: print(next(enfants))

3. ...

4. Pim

5. Pam,

6. Poum

7. Traceback (most recent call last):

8. File "<stdin>", line 2, in <module>

9. StopIteration

10. >>>

1. >>> enfants=iter(("Pim", "Pam", "Poum"))

2. >>> while True:

3. ...     n=next(enfants, None)

4. ...     if n is None: break

5. ...     print(n)

6. ...

7. Pim

8. Pam

9. Poum

10. >>>

À noter que dans Python 2, les itérateurs possèdent aussi une méthode next() qui fait le même travail…

1. # Python 2 uniquement

2. >>> enfants=iter(("Pim", "Pam", "Poum"))

3. >>> while True: print(enfants.next())

4. ...

5. Pim

6. Pam,

7. Poum

8. Traceback (most recent call last):

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

10. StopIteration

… méthode supprimée dans Python 3, car elle faisait doublon.

La fonction iter(callable, sentinelle) crée un itérateur manipulable à partir d'un élément appelable (généralement une fonction) ou renvoie l'exception TypeError si l'élément n'est pas appelable.

L'accès à l'itérateur se fait ensuite au travers d'une simple itération qui prendra fin soit dès que la sentinelle est atteinte si l'élément correspondant est présent, soit au bout de l'itération si celle‑ci est possible.

1. >>> for x in iter(lambda: input("?"), "0"): print(x, type(x))

2. …

3. ? 5

4. 5 <class 'str'>

5. ? 6

6. 6 <class 'str'>

7. ? 7

8. 7 <class 'str'>

9. 0

La fonction enumerate(iterable, start=0) génère une liste de couples (i, x) où i est un nombre énuméré commençant à start et x l’élément correspondant de l'itérable. Permet de gérer en parallèle les éléments d'un tuple ou d'une liste et le compteur de ces éléments.

1. >>> enfants=("Pim", "Pam", "Poum")

2. >>> for (i, x) in enumerate(enfants, 1): print(i, x)

3. ...

4. 1, Pim

5. 2, Pam,

6. 3, Poum

La fonction reversed(iterable) génère une nouvelle séquence contenant l'itérable inversé

1. >>> enfants=("Pim", "Pam", "Poum")

2. >>> for x in reversed(enfants): print(x)

3. ...

4. Poum

5. Pam,

6. Pim

La fonction sorted(iterable, key=None, reverse=False) génère une nouvelle séquence contenant l'itérable trié selon la valeur de la clef.

Si l’argument key est demandé, cet argument devra être une fonction à un paramètre (paramètre présumé être un élément de l’itérable) permettant de considérer selon un algorithme particulier tout élément pour utiliser cette considération comme clef de comparaison. Sinon l’évaluation se fera sur la valeur numérique de l’élément dans son ensemble (les items alphabétiques étant alors convertis en nombres correspondant en Unicode).

Le tri peut être inversé si l'argument reverse est mis à True.

1. >>> auteurs=("Hugo", "Racine", "Balzac")

2. >>> for x in sorted(auteurs, key=lambda x: x[2]): print(x)        # Tri sur le 3° caractère

3. ...

4. Racine

5. Hugo

6. Balzac

À noter : dans Python 2, la fonction sorted() dispose d'un argument supplémentaire cmp=None. Cet argument permet de recevoir une fonction externe programmée pour comparer deux éléments distincts. Cette fonction devant renvoyer un nombre négatif, positif ou nul selon que l’élément 1 est plus petit, plus grand ou égal à l’élément 2. Les éléments à comparer sont alors issus soit du retour de la fonction passée à l'argument key si celui‑ci est présent ; soit de l'itérable lui‑même.

1. # Exemple Python 2 uniquement

3. # La fonction va trier alphabétiquement chaque chaîne inversée ("abc" vu comme "cba")

4. def compare(x, y):

5. if x[::-1] < y[::-1]: return -1

6. if x[::-1] > y[::-1]: return 1

7. return 0

9. >>> auteurs=("Hugo", "Racine", "Balzac")

10. >>> for x in sorted(auteurs, cmp=compare, reverse=True): print x

11. ...

12. Hugo

13. Racine

14. Balzac

L’argument cmp a disparu dans Python 3 parce qu’il était déprécié (dans la majorité des cas on peut s’en sortir avec l’argument key).

1. # Le même exemple Python 3 utilisant l’argument key

3. >>> auteurs=("Hugo", "Racine", "Balzac")

4. >>> for x in sorted(auteurs, key=lambda x: x[::-1], reverse=True): print(x)

5. ...

6. Hugo

7. Racine

8. Balzac

Mais si vraiment cela devient nécessaire, son comportement peut être reproduit en utilisant l'objet cmp_to_key() du module functools (sera vu ultérieurement). Cet objet convertira la fonction en clef, objet qu'on passera alors à l'argument key.

1. # Le même exemple Python 3 utilisant cmp_to_key

3. # La fonction va trier alphabétiquement chaque chaîne inversée ("abc" vu comme "cba")

4. def compare(x, y):

5. if x[::-1] < y[::-1]: return -1

6. if x[::-1] > y[::-1]: return 1

7. return 0

9. from functools import cmp_to_key

11. >>> auteurs=("Hugo", "Racine", "Balzac")

12. >>> for x in sorted(auteurs, key=cmp_to_key(compare), reverse=True): print(x)

13. ...

14. Hugo

15. Racine

16. Balzac

XI - Les générateurs

XI-1 - Introduction

Une fonction pouvant renvoyer tout objet Python peut donc parfaitement renvoyer un tuple ou une liste.

1. def genere_info():

2. return (1, 2, 3, 4, 5)

Toutefois, les valeurs du tuple ou de la liste renvoyés sont figées dans un état immuable. C’est-à-dire que (par exemple) la valeur « 5 » retournée ici continuera à valoir toujours 5, même si elle est traitée bien longtemps après avoir été retournée…

1. for i in genere_info():

2. print(i)

3. …            # Gros traitement de 45 minutes

Lorsque ce sera au tour de la valeur « 5 » d’être traitée, il se sera écoulé 3h depuis sa création.

Ce comportement peut devenir un souci lorsque la pertinence de la valeur dépend du moment non pas de sa création, mais de son utilisation (contenu d’un stock, horodatage, etc.). Dans ce cas, on ne peut plus passer par le fonctionnement habituel du return et il est alors nécessaire de passer par un nouvel outil : le générateur.

Un générateur est une fonction qui génère une information non pas au moment où elle est créée, mais au moment où cette information est demandée. Il se construit en utilisant l’instruction yield.

L’instruction yield se comporte comme un return ; à ceci près que la valeur n’est pas renvoyée à l’appel de la fonction. Elle est seulement mise en préparation. Le renvoi ne sera effectif (et calculé) qu’au moment réel où l'appelant la demandera.

1. def genere_info():

2. yield 1

3. yield 2

4. yield 3

5. yield 4

6. yield 5

8. for i in genere_info():

9. print(i)

10. …            # Gros traitement de 45 minutes

À chaque itération, la valeur sera alors effectivement récupérée depuis le générateur au moment où l’itération en cours correspond au yield prévu à cette itération.

XI-2 - Exemple

Dans les exemples précédents, il n’y a bien évidemment aucune différence concrète entre les deux traitements (dans tous les cas les valeurs sont figées). Cette différence prendra un sens lorsque les valeurs renvoyées seront calculées et que le moment du calcul aura un impact dans la pertinence du résultat.

Dans les deux exemples ci‑dessous qui utilisent tous deux l’heure courante, on admet qu’ils sont lancés tous les deux à 12h précise…

Sans générateur :

1. from datetime import *

2. import time

4. def fct(x):

5. ret=[]

6. for i in range(x):

7. ret.append(datetime.today().strftime("%H:%M:%S"))

8. return ret

10. for (i, f) in enumerate(fct(5), 1):

11. print(i, f)

12. time.sleep(5)

13. print("Terminé: ", datetime.today().strftime("%H:%M:%S"))

Résultat (qui mettra 25 secondes pour s’afficher) :

1. 1 12:00:00

2. 2 12:00:00

3. 3 12:00:00

4. 4 12:00:00

5. 5 12:00:00

6. Terminé: 12:00:25

Avec générateur :

1. from datetime import *

2. import time

4. def fct(x):

5. for i in range(x):

6. yield datetime.today().strftime("%H:%M:%S")

8. for (i, g) in enumerate(fct(5), 1):

9. print(i, g)

10. time.sleep(5)

11. print("Terminé: ", datetime.today().strftime("%H:%M:%S"))

Résultat (qui mettra aussi 25 secondes pour s’afficher) :

1. 1 12:00:00

2. 2 12:00:05

3. 3 12:00:10

4. 4 12:00:15

5. 5 12:00:20

6. Terminé: 12:00:25

Le générateur renvoie l’heure réelle au moment où celle-ci est demandée et non pas au moment où elle est créée.

XI-3 - Différentes actions

Un générateur peut n’être pas forcément traité dans une itération. Le programmeur peut avoir besoin de récupérer à la volée et de façon impromptue une information générée.

Il peut le faire au travers de la fonction next() appliquée au générateur.

1. def genere_info(n):

2. for i in range(n): yield i

4. >>>g=genere_info(5)

5. >>>next(g)

6. 0

7. >>>next(g)

8. 1

9. >>>next(g)

10. 2

11. >>>next(g)

12. 3

Sous Python 2, la fonction next() existe aussi, à la fois sous forme de fonction et sous forme de méthode intégrée au générateur.

1. # Exemple Python 2

3. def genere_info(n):

4. for i in range(n): yield i

6. >>>g=genere_info(5)

7. >>>next(g)

8. 0

9. >>>g.next()

10. 1

11. >>>next(g)

12. 2

13. >>>g.next()

14. 3

Un générateur peut aussi recevoir une valeur lors de son travail.

Cela se fait au travers de la méthode send() du générateur. Cette méthode est alors directement reliée à l’instruction interne yield qui récupère alors la valeur envoyée.

Elle ne peut être invoquée que si le générateur a déjà généré sa première information. Ou alors, elle doit être invoquée avec la valeur None la première fois.

Et son invocation provoque la génération d’une information (tout comme un appel à la fonction next()).

1. def genere_info(n):

2. for i in range(n):

3. x=yield i

4. if x is not None: print("x=[%s]" % x)

6. >>> g=genere_info(5)

7. >>> next(g)

8. 0

9. >>> next(g)

10. 1

11. >>> g.send(100)

12. 2

13. x=[100]

Un générateur peut être fermé depuis l’extérieur avant sa fin naturelle au travers de la méthode close().

1. def genere_info(n):

2. for i in range(n): yield i

4. >>> g=genere_info(5)

5. >>> for i in g:

6. ...    print(i)

7. ...    if i ==2: g.close()

8. ...

9. 0

10. 1

11. 2

Un générateur peut aussi être fermé par lui‑même avant sa fin naturelle via l’instruction return qui ne doit comporter aucune valeur (son but n’est pas de renvoyer une valeur, mais de quitter le générateur).

1. def genere_info(n):

2. for i in range(n):

3. yield i

4. if i == 2: return

6. >>> for i in genere_info(5): print(i)

7. 0

8. 1

9. 2

XI-4 - Quelques précautions

Le générateur possède tout de même un inconvénient : il ne peut être consommé qu’une seule fois.

1. >>> tab=genere_info()

2. >>> print(tuple(tab))

3. (1, 2, 3, 4, 5)

4. >>> print(tuple(tab))

5. ()

Cet inconvénient n'est cependant pas aussi réducteur qu'on pourrait le croire. En effet, en général, l’utilisation courante montre que le générateur n'est réellement utilisé qu'une seule fois. Et dans les rares cas où le résultat du générateur doit-être récupéré dans son ensemble (parce qu’utilisé plusieurs fois), on peut alors tout à fait le stocker dans un tuple ou une liste lors de sa création.

1. >>> tab=tuple(genere_info())

2. >>> print(tab)

3. (1, 2, 3, 4, 5)

4. >>> print(tab)

5. (1, 2, 3, 4, 5)

Dans ce cas, on reproduit alors le comportement standard d’une fonction qui renvoie un tuple avec ses valeurs définitivement figées.

XI-5 - Générateur d’un itérable

Le générateur d’un itérable se traduira de façon classique par une boucle sur l’itérable avec chaque élément renvoyé par yield.

1. def genere_info():

2. for i in range(5): yield i

4. >>> for i in genere_info(): print(i)

5. 0

6. 1

7. 2

8. 3

9. 4

Python 3 offre un raccourci syntaxique permettant de simplifier ce code en remplaçant la boucle par un générateur directement placé sur l’itérable. Cela se fait avec l’instruction yield from.

1. def genere_info():

2. yield from range(5)

4. >>> for i in genere_info(): print(i)

5. 0

6. 1

7. 2

8. 3

9. 4

À noter que cette instruction n’est pas disponible sous Python 2.

XII - La portée et la visibilité des variables

XII-1 - La portée

Avec l’avènement des langages structurés s’est posé pour les variables le problème de la portée (partie du code où une variable garde sa valeur) et de la visibilité (partie du code où une variable est accessible).

Pour bien comprendre comment Python résout ces problèmes, il faut d’abord comprendre la notion de « scope ».

Le « scope » est la zone de visibilité d’un code Python. Python ne connait que 2 scopes :

- le scope dit global, qui contient la globalité du code Python (tout le code source),

- le scope dit local, qui ne concerne que les parties de codes situées dans une fonction ou un objet (sera vu ultérieurement). Bien évidemment, s’il n’y a qu’un seul scope global, il y a autant de scope locaux différents qu’il y a de fonctions ou objets distincts.

XII-2 - La visibilité

XII-2-a - Principes

Une variable définie dans un scope est alors visible et accessible depuis tout ce scope. Cela signifie donc qu’une variable définie dans le scope global est alors visible de tout le code Python y compris dans tout scope local c’est-à-dire y compris dans toute fonction ou objet.

1. >>> def fct(): print(toto)      # Affichage d’une variable du scope global

2. ...

3. >>> toto=123                    # Création de la variable dans le scope global

4. >>> print(toto)                 # Vérification de son contenu

5. 123

6. >>> fct()                       # Appel de la fonction qui affiche la variable

7. 123

De même, une variable définie dans le scope local d’une fonction est alors accessible depuis tout le code de la fonction ; y compris dans une sous‑fonction éventuelle.

1. >>> def fct():                  # Création d'une fonction et de son scope local

2. ...    def xxx(): print(toto)   # Affichage d’une variable du scope local de la fonction

3. ...    toto=200                 # Création de la variable dans le scope local de la fonction

4. ...    xxx()                    # Appel de la sous-fonction qui affiche la variable

5. ...

6. >>> fct()                       # Appel de la fonction

7. 200

Toutefois, il faut que l’instruction de création soit quand même atteinte pour que la variable soit créée.

1. >>> if True: titi=123          # Comme le test est vrai, la variable "titi" est créée

3. ...

4. >>> if False: tata=456         # Comme le test est faux, la variable "tata" n’est pas créée

6. ...

7. >>> print(titi)

8. 123

9. >>> print(tata)

10. Traceback (most recent call last):

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

12. NameError: name 'tata' is not defined

Redéfinir dans le scope local une variable déjà existante dans le scope global ne gêne pas. Dans ce cas, la variable située dans le scope local prendra le pas sur la variable du scope global ; laquelle redeviendra accessible sitôt le scope local terminé.

1. >>> def fct():                 # Création d'une fonction et de son scope local

2. ...    toto=200                # Création d’une variable locale qui masque la globale

3. ...    print(toto)             # Affichage de la variable locale

4. ...

5. >>> toto=100                   # Variable globale

6. >>> print(toto)                # Vérification de son contenu

7. 100

8. >>> fct()                      # Appel de la fonction qui utilise sa propre variable

9. 200

10. >>> print(toto)                # Fonction terminée, la variable globale est redevenue accessible

11. 100

Par défaut, au sein d’un scope local, une variable provenant du scope global n’est accessible qu’en lecture seule…

1. >>> def fct():                 # Création d'une fonction et de son scope local

2. ...    print(toto)             # Affichage de la variable globale

3. ...    toto=200                # Tentative de modifier la variable globale => erreur

4. ...

5. >>> toto=100

6. >>> fct()

7. Traceback (most recent call last):

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

9. File "<stdin>", line 2, in fct

10. UnboundLocalError: local variable 'toto' referenced before assignment

… à moins qu’on ne spécifie explicitement avec l'instruction global que l’on veut pouvoir accéder pleinement à la variable située dans le scope global.

1. >>> def fct():                 # Création d'une fonction et de son scope local

2. ...    global toto             # Demande d'accès complet à la variable globale

3. ...    print(toto)             # Affichage de la variable globale

4. ...    toto=200                # Modification de la variable globale

5. ...

6. >>> toto=100

7. >>> fct()

8. 100

9. >>> print(toto)

10. 200

Attention, recourir de façon continue à cette technique peut causer plus de soucis que ce que ça n’en résout. La majorité des programmeurs professionnels évitent donc l'utilisation des variables globales qui cassent l'indépendance d'une fonction vis-à-vis de son environnement (si la variable globale change de forme ou de nom, il faut alors passer en revue tous les endroits où elle est utilisée) et qui ne sont pas contrôlées (si la valeur d'une globale change par erreur, difficile alors de trouver l'endroit du changement).

Par ailleurs, il n’y a pas de notion de « hiérarchie » au sein des scopes locaux. Faire appel au mot clef global dans une sous‑fonction fera référence au seul et unique scope global du programme et non au scope de la fonction supérieure (qui, dans une autre logique, aurait pu être vu comme « global » dans la fonction).

1. >>> def fct():                        # Création d'une fonction et de son scope local

2. ...    def xxx():                     # Création d'une sous-fonction qui utilise le même scope

3. ...        global toto                # Référence au seul et unique scope global

4. ...        toto=300                   # Modification de la variable globale

5. ...        print("xxx:", toto)        # Affichage de la variable globale

6. ...    toto=200                       # Variable locale à la fonction

7. ...    xxx()                          # Appel de la fonction qui modifie la globale

8. ...    print("fct:", toto)            # Affichage de la variable locale

9. ...

10. >>> toto=100

11. >>> print(toto)

12. 100

13. >>> fct()

14. xxx: 300

15. fct: 200

16. >>> print(toto)

17. 300

Attention, toute référence à une variable du scope global interdit ensuite sa redéfinition dans le scope local.

1. >>> def fct():                        # Création d'une fonction et de son scope local

2. ...    print(toto)                    # Affichage de la variable du scope global

3. ...    toto=200                       # Tentative de création d'une variable locale => erreur

4. ...

5. >>> toto=100

6. >>> fct()

7. Traceback (most recent call last):

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

9. File "<stdin>", line 2, in fct

10. UnboundLocalError: local variable 'toto' referenced before assignment

En fait, avec ce genre de syntaxe, Python croit qu'on tente de modifier la variable du scope global sans l’avoir demandé avec l'instruction global.

Le contenu complet des scopes local et global peut se récupérer au travers des fonctions locals() et globals(). Ces deux fonctions renvoient les dictionnaires associés à ces deux scopes ayant les variables pour clefs et leurs valeurs comme valeur…

1. >>> toto=300

2. >>> globals()["toto"]

3. 300

4. >>> locals()["toto"]

5. 300

… et ces dictionnaires peuvent aussi être modifiés ce qui se répercute sur les variables par ricochet.

1. >>> toto=300

2. >>> toto

3. 300

4. >>> locals()["toto"]=400

5. >>> toto

6. 400

XII-2-b - Conséquences

Cette notion de scope entraine beaucoup de conséquences. La première c’est que contrairement à d’autres langages (comme le C), une variable n’est pas limitée à son bloc de travail, mais existe dans tout le scope dudit bloc.

Et donc, dans la séquence suivante…

1. >>> for i in range(10): pass

2. ...

3. >>> print(i)

4. 9

… la variable i existe même en dehors de la boucle. C’est d’ailleurs ce qui permet à ce genre d’instructions…

1. for i in range(10):

2. if …(test quelconque vrai)…: trouve=True

3. else:

4. trouve=False

6. print(trouve)

… de fonctionner correctement.

De plus, cela peut amener des effets de bord difficilement compréhensibles, surtout quand on définit des fonctions utilisant des variables venues du scope global.

Prenons par exemple un tableau de fonctions qui est créé au fur et à mesure ; où chaque fonction créée utilise dans son traitement la variable de boucle du scope global :

1. >>> tabF=[]

2. >>> for i in range(3):

3. ...    def my_func(x):

4. ...        return x+i

5. ...    tabF.append(my_func)

Comme on le voit, le tableau contient différentes fonctions qui sont censées ajouter chacune l'indice de leur rang au paramètre reçu. Et donc tabF[0] ajoutera 0 à son paramètre, tabF[1] ajoutera 1 etc.

Ainsi, si on demande à toutes les exécuter avec le paramètre 50, on s’attend à voir s’afficher des nombres croissants à partir de ce nombre (50, 51, 52, etc.).

Or, au résultat, ce n’est pas du tout ce qui se passe…

1. >>> for f in tabF:

2. ...    print(f, f(50))

3. <function my_func at 0x7f57fc4f16e0>, 52

4. <function my_func at 0x7f57fc4f1758>, 52

5. <function my_func at 0x7f57fc4f17d0>, 52

L’explication est que chaque fonction additionne au paramètre reçu la valeur de i à son état actuel (tel qu'il est en fin de boucle range(3)) et non la valeur de i au moment où la fonction a été créée. On peut s’en rendre compte plus aisément si on rajoute par exemple l’instruction i=123 avant de faire le test (on aura 125, 125, 125).

Pour s’en sortir, il est impératif de ne pas utiliser de valeur extérieure à la fonction, mais un paramètre local, même si ce paramètre se contente de récupérer la variable en cours d’itération…

1. >>> tabF=[]

2. >>> for i in range(3):

3. ...    def my_func(x, y=i):

4. ...        return x+y

5. ...    tabF.append(my_func)

… ce qui donne ainsi le résultat attendu.

1. >>> for f in tabF:

2. ...    print(f, f(50))

3. <function my_func at 0x7f57fc4f16e0>, 50

4. <function my_func at 0x7f57fc4f1758>, 51

5. <function my_func at 0x7f57fc4f17d0>, 52

Et pour finir, rien n’interdit d’utiliser en interne le même nom de paramètre que celui de la variable en cours d’itération ; et de remplacer la définition de la fonction par une lambda.

1. >>> tabF=[]

2. >>> for i in range(3):

3. ...    tabF.append(lambda x, i=i: x+i)

XII-3 - La portée

À l'instar d’autres langages (comme le static du C), Python permet de pérenniser la valeur d'une variable en dehors de son scope au travers du mécanisme d'un argument par défaut d'une fonction positionné sur un objet mutable (liste, dictionnaire).

En effet, l'objet par défaut étant constitué lors de la création de la fonction et non à son appel, il conserve sa valeur entre chaque appel (cf. chapitre sur les paramètres par défauts d'une fonction).

1. >>> def fct(save=[]):

2. ...    toto=save.pop() if save else 0

3. ...    toto+=1

4. ...    print(toto)

5. ...    save.append(toto)

7. >>> fct()

8. 1

9. >>> fct()

10. 2

11. >>> fct()

12. 3

Remarque : on peut tout de même se demander alors l'utilité de la variable toto dans ce cas…

XII-4 - Identité et hash d’une variable

XII-4-a - Identité d’une variable

Toute variable possède une identité. Il s’agit d’un entier garantissant l’unicité de la variable pour toute sa durée de vie. Si l’on veut essayer de faire une analogie sommaire avec le C, on peut, dans une certaine mesure, parler alors d’adresse.

La récupération de cette identité peut se faire par la fonction id().

1. >>> a=500

2. >>> b=600

3. >>> c=3.14

4. >>> id(a)

5. 140047619882064

6. >>> id(b)

7. 140047620423600

8. >>> id(c)

9. 140047620461072

Il est intéressant de noter que toute variable créée par copie aura alors le même identifiant que la variable d'origine. Et ce, jusqu'à ce qu'elle subisse une nouvelle affectation (même identique).

1. >>> a=3.14

2. >>> b=a

3. >>> id(a)

4. 140047620461144

5. >>> id(b)

6. 140047620461144

7. >>> b=3.14

8. >>> id(b)

9. 140047620461072

La fonction id() peut aussi être appliquée à des constantes pures, mais les résultats de cette opération (qui changent avec l'évolution du code) ne veulent rien dire de significatif.

1. >>> id(500)

2. 140047619882064

3. >>> id(600)

4. 140047619882064

5. >>> id(700)

6. 140047619882064

7. >>> a=5

8. >>> id(700)

9. 140047619881008

Et enfin, les valeurs comprises entre ‑5 et 256 sont stockées « en dur » dans le moteur Python. Toute variable ayant l’une de ces valeurs aura alors toujours le même identifiant.

1. >>> a=5

2. >>> b=5

3. >>> id(a)

4. 10771648

5. >>> id(b)

6. 10771648

7. >>> b=6

8. >>> c=6

9. >>> id(b)

10. 10771680

11. >>> id(c)

12. 10771680

13. >>> id(6)

14. 10771680

Remarque : si la valeur 256 peut se comprendre de par sa signification bien connue de tous les développeurs, en revanche, en ce qui concerne la raison des valeurs négatives allant jusqu'à ‑5nous n’avons pas d’explication.

XII-4-b - Hash d’une variable

En plus de son identité, toute variable possède un « hash » c’est-à-dire une méthode calculant son empreinte numérique. Tout comme pour l’identité, il s’agit aussi d’un entier permettant de comparer rapidement les choses (si deux hashs sont égaux alors les valeurs dont ils sont issus le sont aussi).

La récupération de ce hash peut se faire par la fonction hash().

1. >>> a="toto"

2. >>> b="titi"

3. >>> c=tuple((1, 2, 3))

4. >>> hash(a)

5. -6444533920638468822

6. >>> hash(b)

7. 4818377268154382741

8. >>> hash(c)

9. 2528502973977326415

Remarque : la fonction hash() ne peut s’appliquer qu’aux types immuables. Les listes, ensembles et dictionnaires en sont donc exclus. En fait, une valeur ne peut servir de clef à un dictionnaire que si elle est « hashable ».

1. >>> hash(list())

2. Traceback (most recent call last):

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

4. TypeError: unhashable type: 'list'

5. >>> d={set() : "xxx"}

6. Traceback (most recent call last):

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

8. TypeError: unhashable type: 'set'

XII-5 - De la bonne façon de copier les choses

Le paragraphe précédent permet de mieux comprendre le mécanisme d’affectation de variables en Python et ce qui en découle. Quand on écrit (par exemple) a=345 puis b=a, Python dépose un nombre 345 dans une case mémoire puis lui assigne une étiquette nommée a et une autre nommée b. C'est la raison pour laquelle id(a) et id(b) affichent la même chose.

Remarque : si ensuite on écrit b=345, Python dépose 345 dans une seconde case et lui réassigne l’étiquette b. Il ne va évidemment pas se fatiguer à vérifier si 345 a déjà été déposé quelque part, ce serait une perte de temps avec trop peu de plus‑value (à l'exception des nombres entre -5 et 256 dont on a déjà parlé au paragraphe précédent).

Alors ce mécanisme d'affectation de différentes étiquettes à une même case mémoire ne pose absolument aucun souci quand il s'agit de types immuables (int, float, str, tuple). Pour ces types, écrire a=345 puis b=a n'empêchera pas d'écrire ensuite a=678. Python se comportera parfaitement à propos de a et de b qui auront leurs valeurs adéquates et personnelles.

Là où les soucis arrivent, c'est quand il s'agit de types mutables (listes, ensembles, dictionnaires et leurs dérivés). En effet, qui dit « type mutable » dit « modification de son intégrité ». Or, tout comme pour les types simples, le type mutable n'est créé qu'une seule fois en mémoire et ensuite, les diverses variables viennent s'y adresser.

Ainsi, écrire a=[1, 2, 3] puis b=a fait pointer ces deux variables vers la même zone mémoire. Et si cette zone est modifiée, la modification se répercute sur les deux variables.

1. >>> a=[1, 2, 3]

2. >>> b=a

3. >>> a.append("toto")

4. >>> print(a, id(a))

5. ([1, 2, 3, 'toto'], 139969669202184)

6. >>> print(b, id(b))

7. ([1, 2, 3, 'toto'], 139969669202184)

Cela ne veut pas dire qu'il est interdit d'écrire b=a, juste qu'il faut savoir ce que cela entraine. C'est un peu le même souci qu'avec les paramètres par défaut des fonctions quand on veut y mettre un type mutable (cf. chapitre sur les fonctions et les valeurs par défaut des arguments).

Néanmoins, il est possible, si nécessaire, de créer une vraie copie d'un type mutable, copie ayant alors ensuite sa vie propre. Si par exemple il s'agit de listes, il suffit d'utiliser le slice, la fonction list() ou plus récemment la méthode copy() pour créer cette copie.

1. >>> a=[1, 2, 3]

2. >>> b=a[:]

3. >>> c=list(a)

4. >>> d=a.copy()

5. >>> a.append("pim")

6. >>> b.append("pam")

7. >>> c.append("poum")

8. >>> d.append("toto")

9. >>> print(a, id(a))

10. ([1, 2, 3, 'pim'], 139977338712392)

11. >>> print(b, id(b))

12. ([1, 2, 3, 'pam'], 139977339206280)

13. >>> print(c, id(c))

14. ([1, 2, 3, 'poum'], 139977338007624)

15. >>> print(d, id(d))

16. ([1, 2, 3, 'toto'], 139977337915272)

Et pour les autres types (ensembles, dictionnaires), s'il n'y a pas le slice, il reste quand même leurs fonctions dédiées (set() et dict()) et aussi leurs propres méthodes copy().

Attention : ces méthodes ne fonctionnent qu'à condition que les éléments de premier niveau soient eux-mêmes immuables. Cela fonctionnera donc parfaitement pour une liste d'entiers où tous les entiers de la liste d'origine seront recopiés dans la liste de destination ; mais cela ne fonctionnera pas pour une liste de listes, car là, seules les références des sous-listes composant la liste de base seront copiées dans la liste de destination. Dans ce dernier cas, le problème se reposera si on tente ensuite de donner une vie différente à l'une des sous-listes.

1. >>> a=[[1, 2, 3], [4, 5, 6]]

2. >>> b=a.copy()

3. >>> a.append("Pim")

4. >>> b.append("Pam")

5. >>> print(a)

6. [[1, 2, 3], [4, 5, 6], 'Pim']

7. >>> print(b)

8. [[1, 2, 3], [4, 5, 6], 'Pam']

9. # Jusque là tout est ok

11. # Tentons maintenant de modifier une sous-liste de la première variable…

12. >>> a[0][0]="Poum"

13. >>> print(a)

14. [['Poum', 2, 3], [4, 5, 6], 'Pim']

15. >>> print(b)

16. [['Poum', 2, 3], [4, 5, 6], 'Pam']

Pour régler ce type de problèmes, il faut partir dans des opérations à base de récursivité. Il existe toutefois un module « copy » (les modules seront vus ultérieurement) qui contient des outils permettant ce travail de copie indépendante et complète.

Rappel : tous ces soucis concernant la modification d’un original qui peut se répercuter sur sa copie n'existent pas avec une chaîne ou un tuple qui ne sont pas modifiables (immuable).

XIII - Les listes en intension

La notion de « liste en intension » (appelée aussi parfois « liste en compréhension ») exprime une forme d’écriture permettant de générer de nouvelles listes directement par traitement de listes existantes. Le terme « liste » doit être pris ici au sens large (englobant donc aussi les tuples, les ensembles et les dictionnaires et de façon plus générale tout ce qui est itérable).

Le terme « intension » (à ne pas confondre avec « intention ») se traduit par « conception par définition » et s’oppose à « extension » (qui se traduit, lui, par « conception par l’exemple »).

La syntaxe générale est de remplacer les éléments situés entre les crochets par une instruction de création faisant appel à une liste déjà existante.

Exemple : partons d'une liste de base contenant les entiers de 0 à 9…

1. >>> tab=list(range(10))

2. >>> tab

3. [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Demandons ensuite à créer une seconde liste issue de la première, mais ne contenant que les éléments pairs.

De ce qu’on a vu jusqu’à présent, la méthode habituelle serait d’amorcer une seconde liste vide et la remplir à partir de la première en évaluant chaque élément un à un…

1. >>> pair=[]

2. >>> for x in tab:

3. ...    if (x%2) == 0: pair.append(x)

4. ...

5. >>> pair

6. [0, 2, 4, 6, 8]

Python offre toutefois pour ce faire une syntaxe plus concise permettant de créer directement la seconde liste à partir des éléments de la première, éléments traités alors directement dans l’instruction de création.

1. >>> pair=[x for x in tab if (x%2) == 0]

2. >>> pair

3. [0, 2, 4, 6, 8]

De là, on peut continuer en demandant par exemple à créer une troisième liste, toujours issue de la première, mais ne contenant cette fois que les éléments impairs et mis au carré…

1. >>> carre_impair=[x**2 for x in tab if (x%2) != 0]

2. >>> carre_impair

3. [1, 9, 25, 49, 81]

Cette écriture, qui permet de créer des listes avec une grande concision, tend à se rapprocher de la notation utilisée en mathématiques dans la théorie des ensembles.

- Écriture mathématique : carre_pair={x²|x ∈ tab, [x]2 = 0}

- Écriture python : carre_pair=[x**2 for x in tab if (x%2) == 0]

Le principe général est d’inclure la boucle dans la création de la seconde liste en y rajoutant ensuite les évaluations et traitements éventuels.

1. # Algorithme classique

2. >>> res=[]

3. >>> for x in orig:

4. ...    if test(x) == v:

5. ...        res.append(fct(x))

6. ...

1. # Algorithme en compréhension

2. >>> res=[fct(x) for x in orig if test(x) == v]

L’un des avantages de cette écriture est qu’elle supprime l’utilisation du list.append() qui peut être très gourmand en ressources.

Elle fonctionne à partir de tout itérable et peut générer tout aussi bien des listes que des tuples, des ensembles ou des dictionnaires.

1. >>> tab=range(10)

2. >>> carre_pair=list(x**2 for x in tab if (x%2) == 0)

3. >>> carre_pair

4. [0, 4, 16, 36, 64]

5. >>>

6. >>> cube_impair=dict((str(x), x**3) for x in tab if (x%2) != 0)

7. >>> cube_impair

8. {'1': 1, '3': 27, '9': 729, '5': 125, '7': 343}

9. >>>

10. >>> domino=tuple((x, y) for x in range(7) for y in range(x+1))

11. >>> domino

12. ((0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2), (3, 0), (3, 1), (3, 2), (3, 3), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (6, 0), (6, 1), (6, 2), (6, 3), (6, 4), (6, 5), (6, 6))

À noter : la ou les variable(s) interne(s) utilisée(s) pour itérer la liste n’influe(nt) pas sur les variables externes même si des variables de même nom existent…

1. >>> x=1234

2. >>> tab=range(5)

3. >>> carre=tuple(x**2 for x in tab)

4. >>> carre

5. (0, 1, 4, 9, 16)

6. >>> x

7. 1234

… inversement, les variables externes à l’expression sont accessibles dans l’expression si celle‑ci ne redéfinit pas des variables de même nom.

1. >>> toto=500

2. >>> tab=range(5)

3. >>> carre=tuple((x**2, toto) for x in tab)

4. >>> carre

5. ((0, 500), (1, 500), (4, 500), (9, 500), (16, 500))

En fait, la ou les variables internes utilisées pour itérer la liste sont créées à chaque itération et ceci de façon distincte d’une éventuelle variable externe portant le même nom :

1. >>> x=1234

2. >>> id(x)

3. 140237834012528

4. >>> tab=range(5)

5. >>> carre=tuple((id(x), x**2) for x in tab)

6. >>> carre

7. ((10771488, 0), (10771520, 1), (10771552, 4), (10771584, 9), (10771616, 16))

8. >>> id(x)

9. 140237834012528

XIV - Les classes

XIV-1 - Préambule

Les classes en Python permettent de représenter des objets au sens informatique du terme. Comme telles, elles possèdent donc des attributs (des variables qui les caractérisent) et des méthodes (des fonctions qui permettent de les manipuler).

Une classe se définit en Python par le mot‑clef class suivi du nom que l’on désire lui donner. Sans oublier évidemment les deux‑points habituels terminant toute définition de bloc.

1. >>> class toto:

2. ...    pass                                # Objet "toto" (vide)

3. ...

4. >>> a=toto()

5. >>> a

6. <__main__.toto instance at 0x7fc685a86290>

Avec l'objet, on définit généralement un constructeur (méthode automatiquement appelée à la création de l'objet) et un destructeur (méthode automatiquement appelée à sa suppression). Le constructeur est utilisé généralement pour associer à l’objet ses caractéristiques initiales ; et le destructeur sert à libérer les ressources système éventuellement utilisées durant la vie de l'objet.

Le constructeur se définit par la méthode __init__() ; et le destructeur par la méthode __del__(). Ces deux méthodes (tout comme d'ailleurs toutes les méthodes de l'objet) étant à la base des fonctions, elles doivent être précédées du mot-clef def déjà vu ; et doivent impérativement définir au moins un argument ; argument représentant l’instance de l'objet lui‑même. En effet, lorsqu’une instance appelle une fonction méthode de l’objet, cette instance est récupérée par la méthode qui peut alors la manipuler. Par convention, cet argument est nommé self.

1. >>> class personne:

2. ...    def __init__(self, nom):

3. ...        print("Création de l'objet %s avec un paramètre [%s]" % (self, nom))

4. ...    def __del__(self):

5. ...        print("Suppression de l’objet %s" % self)

7. >>> p=personne("xxx")

8. Création de l’objet <__main__.personne instance at 0x7fc685a86290> avec un paramètre [xxx]

9. >>> p

10. <__main__.personne instance at 0x7fc685a86290>

11. >>> del p

12. Suppression de l’objet <__main__.personne instance at 0x7fc685a86290>

XIV-2 - L’instanciation et les instances

À ce point, il convient de faire un bref rappel sur les termes dédiés à l’objet en informatique.

Une classe, c’est à la base un type spécifique défini par l’utilisateur (représentant alors un objet). Une fois l'objet (le type) défini, il est possible de créer des variables de ce type. En effet, de même qu’on peut créer des variables de type string, entier ou flottant, on peut aussi bien évidemment créer des variables du type de l’objet que l’on a défini.

Le mécanisme de création d’une variable d’un type « objet » se nomme « instanciation ».

Une fois la variable créée, elle est du type de l’objet en question. On nomme alors cette variable une « instance » (ou, terme plus complet, « instance de classe »).

1. >>> class toto: pass                      # Le même objet "toto" (toujours vide)

2. ...

3. >>> var=toto()                            # Ici se fait l’instanciation

4. >>> var                                   # "var" instance de "toto"

5. <__main__.toto instance at 0x7fc685a86290>

Une instance de classe, c’est simplement une variable du type de la classe en question.

1. >>> type(var)

2. <class 'instance'>

XIV-3 - Les attributs

Un attribut est une variable située dans l'objet et qui permet de caractériser une instance d'une autre variable. Deux variables par exemple, toutes deux instances d'un objet « cercle », pourront ne pas avoir les mêmes caractéristiques (rayon différent pour chacune). Pour arriver à faire cette différence, il est nécessaire que l’objet « cercle » possède un attribut « rayon » permettant de stocker en interne sa caractéristique qui le fait différer d’un autre.

Au sein de l'objet lui‑même, un attribut se définit avec la variable représentant l’instance de l’objet (donc self). Et le lien entre l’objet et l’attribut se fait avec l’opérateur « point » (.).

1. >>> class personne:

2. ...    def __init__(self, nom):

3. ...        self.nom=nom

4. ...        print("Création de l'objet %s avec un paramètre [%s]" % (self, nom))

5. ...

6. >>> p=personne("xxx")

7. Création de l’objet <__main__.personne instance at 0x7fc685a86290> avec un paramètre [xxx]

8. >>> p

9. <__main__.personne instance at 0x7fc685a86290>

À partir de là, l'attribut nom est récupérable et utilisable depuis toute variable instance de cet objet. Et même modifiable.

1. >>> p.nom

2. 'xxx'

3. >>> p.nom="toto"

4. >>> p.nom

5. 'toto'

Contrairement à beaucoup de langages où les attributs doivent être impérativement déclarés à l’avance dans leurs classes ; Python autorise la création directe d’un nouvel attribut depuis l’instance elle‑même…

1. >>> p.prenom="Arthur"

2. >>> p.nom

3. 'toto'

4. >>> p.prenom

5. 'Arthur'

… ce qui est rarement une bonne chose !!!

1. >>> p1=personne("LeRoi")

2. >>> p1.prenom="Arthur"

3. >>> p2=personne("Autre")

4. >>>

5. >>> for x in (p1, p2):

6. ...    print(x.nom, x.prenom)

7. LeRoi, Arthur

8. Traceback (most recent call last):

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

10. AttributeError: 'personne' object has no attribute 'prenom'

Il est donc préférable de créer tous les attributs d’un objet dans l’objet lui‑même. Après tout, ce n’est pas pour rien qu’un objet possède un constructeur.

XIV-4 - Les méthodes

Une méthode est une fonction permettant d'effectuer un travail sur (ou en relation avec) l'objet. Il s'agit en fait d'une fonction définie dans l'objet. Attention donc à ne pas oublier le mot-clef def et à ne pas oublier que cette fonction (méthode) reçoit toujours en premier paramètre l'instance de l'objet lui-même (par conséquent, ne pas oublier le self) ; ce qui permet donc à la méthode d’accéder à l’objet qui l’invoque et à ses attributs. Sans oublier là encore les deux‑points habituels.

1. >>> class rectangle():

2. ...    def __init__(self, height; width):

3. ...        self.height=height

4. ...        self.width=width

5. ...    def diagonale(self):

6. ...        return (self.height ** 2 + self.width ** 2) ** 0.5

7. ...

8. >>> p=rectangle(3, 4)

9. >>> p.diagonale()

10. 5.0

En fait, cette récupération obligatoire de l’instance dans le premier paramètre de la méthode s’explique par le fait que l’instruction instance.methode() n’est qu’un raccourci de l’écriture littérale classe.methode(instance).

1. >>> p=rectangle(3, 4)

2. >>> rectangle.diagonale(p)

3. 5.0

À noter que __init__() et __del__() vues précédemment, étant elles aussi des fonctions permettant d'effectuer un travail sur l’objet, sont donc elles aussi des méthodes.

XIV-5 - La surcharge de méthodes

La surcharge de méthode (appelée aussi surdéfinition ou polymorphisme) est une possibilité offerte par certains langages de programmation de définir des méthodes de même nom, mais qui diffèrent par le nombre ou le type des paramètres effectifs (signature).

Python ne permet malheureusement pas la surcharge de méthodes au sein d’une même classe. La seule façon en Python de pouvoir coder plusieurs actions distinctes dans une méthode selon le nombre ou le type des paramètres passés à ladite méthode est de jouer avec les valeurs par défaut ou de vérifier le type en question dans la méthode qui elle, ne peut qu’être unique.

1. >>> class nbr:

2. ...    def __init__(self, p):

3. ...        self.p=p

4. ...    def div(self, q=1):

5. ...        if type(q) == int: return self.p // q

6. ...        if type(q) == float: return self.p / q

7. ...        if type(q) == str: return q

8. ...        return None

9. ...

10. >>> n=nbr(5)

11. >>> n.div()

12. 5

13. >>> n.div(2)

14. 2

15. >>> n.div(2.0)

16. 2.5

17. >>> n.div("toto")

18. 'toto'

19. >>> n.div(dict())

20. None

XIV-6 - Les attributs et méthodes privés

XIV-6-a - Privatisation par Python

Dans les exemples précédents, tout attribut créé dans l’objet devient élément accessible de l’objet et peut même être modifié depuis l'extérieur de l’objet. C'est ce qu'on nomme communément un « attribut public ».

Il est souvent recommandé en programmation objet de ne pas donner d’accès public aux attributs d’un objet (aucun contrôle sur les modifications par autrui) et de préférer des attributs non accessibles depuis l'extérieur (attributs « privés »).

On peut privatiser un attribut en faisant précéder son nom de deux underscores accolés __).

1. class personne:

2. def __init__(self, nom):

3. self.__nom=nom

4. print("Je crée l'objet %s avec un paramètre [%s] " % (self, nom))

6. >>> p=personne("xxx")

7. Je crée l’objet <__main__.personne instance at 0x7fc685a86290> avec un paramètre [xxx]

8. >>> p

9. <__main__.personne instance at 0x7fc685a86290>

L’attribut reste accessible depuis l’intérieur de l’objet (donc de toutes ses méthodes), mais sera caché à tout ce qui y est extérieur (y compris malheureusement pour celui qui en aurait légitimement besoin).

1. class personne:

2. def __init__(self, nom):

3. self.__nom=nom

4. def affich(self):

5. print("nom=%s" % self.__nom)

7. >>> p=personne("xxx")

8. >>> p

9. <__main__.personne instance at 0x7fc685a86290>

10. >>> p.affich()

11. nom=xxx

12. >>> p.nom

13. Traceback (most recent call last):

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

15. AttributeError: 'personne' object has no attribute 'nom'

16. >>> p.__nom

17. Traceback (most recent call last):

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

19. AttributeError: 'personne' object has no attribute '__nom'

La notion de privatisation peut aussi s’étendre aux méthodes, en faisant précéder elles aussi leur nom de deux underscores. Cela rend donc la méthode non visible depuis l’extérieur de l’objet (permet par exemple de créer des méthodes dédiées à certains traitements internes non nécessaires aux utilisateurs de l’objet). À noter que le constructeur __init__() et le destructeur __del__(), qui, même si cela ressort mal à l’impression, ont bel et bien eux aussi deux underscores devant leur nom, sont donc eux aussi des méthodes privées.

La façon qu’a Python de cacher l’attribut ou méthode privé reste cependant très basique. Il transforme le nom de l’attribut en « underscore + classe + underscore + underscore + nom » (mécanisme appelé « name mangling » en anglais) ; le but étant de protéger l’attribut (ou la méthode) contre une erreur de programmation et non contre une malveillance volontaire (philosophie Python : les développeurs Python sont adultes et consentants).

Cela signifie que l’attribut (ou la méthode) reste accessible pour qui veut s’en donner la peine…

1. >>> p=personne("xxx")

2. >>> p._personne__nom

3. 'xxx'

4. >>> p._personne__nom="toto"

5. >>> p._personne__nom

6. 'toto'

… mais introduit des contraintes de maintenance liées à l’évolutivité du code (si le nom de l’objet change ou dans le cas d’un travail collaboratif entre divers intervenants par exemple).

XIV-6-b - Accès aux attributs privés

En dehors de cette manière particulière (et peu recommandée) d’accéder aux éléments privés en utilisant leur nom obfusqué, le propriétaire de l'objet pourra de son côté, s’il le désire, offrir un accès spécifique aux attributs privés de son objet en créant des accesseurs (getter/setter/deleter).

Ce getter/setter/deleter se crée en deux étapes :

- écriture des fonctions dédiées aux différents accès (lecture=getter, écriture=setter, suppression=deleter) de l’attribut,

- encapsulation de ces fonctions dans l’instruction property(getter, setter, deleter) qu’on affecte alors à, ce qui sera vu depuis l’extérieur, comme un simple attribut manipulable.

1. class personne:

2. # Écriture des fonctions dédiées

3. def __get_nom(self): return self.__nom

4. def __set_nom(self, nom): self.__nom=nom

5. def __del_nom(self): del self.__nom

7. # Encapsulation de ces fonctions dans la fonction property()

8. nom=property(__get_nom, __set_nom, __del_nom)

10. # Constructeur

11. def __init__(self, nom): self.__nom=nom

L’attribut nom ressemble à un attribut ordinaire, mais correspond en réalité à un point d’entrée vers le getter, le setter et le deleter. Tout accès en lecture à cet attribut appellera en interne le getter spécifié ; tout accès en écriture (affectation) appellera le setter et toute demande de suppression appellera le deleter.

1. >>> p=personne("xxx")

2. >>> p.nom

3. 'xxx'

4. >>> p.nom="toto"

5. >>> p.nom

6. 'toto'

7. >>> del p.nom

8. >>> p.nom

9. Traceback (most recent call last):

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

11. File "<stdin>", line 3, in __get_nom

12. def __get_nom (self): return self.__nom

13. AttributeError: 'personne' object has no attribute '_personne__nom'

14. >>>

La différence avec un attribut public ne se perçoit pas immédiatement (c'est d'ailleurs le but voulu avec la fonction property()). L’utilisateur continue à accéder aux attributs comme s’ils étaient publics.

Toutefois, cela permet au programmeur de l'objet de contrôler ses entrées/sorties d'attributs…

1. class personne:

2. def __set_nom(self, nom):

3. if nom == "":

4. print("Pas de nom vide !!!")

5. return

6. print("ok %s" % nom)

7. self.__nom=nom

8. nom=property(

9. lambda self: self.__nom,         # Getter intégré dans la lambda

10. __set_nom,                       # Setter

11. None,                            # Deleter (pas de deleter)

12. )

14. # Constructeur

15. def __init__(self, nom): self.__nom=nom

17. >>> p=personne("xxx")

18. >>> p.nom

19. 'xxx'

20. >>> p.nom=""

21. Pas de nom vide !!!

22. >>> p.nom

23. 'xxx'

24. >>> p.nom="toto"

25. ok toto

26. >>> p.nom

27. 'toto'

… ou de faire l’impasse sur un ou deux des trois éléments getter/setter/deleter si l’opération s’y rapportant n’est pas envisagée (l’élément qui n’y est pas étant alors remplacé par None dans la fonction property()).

1. class personne:

2. # Getter seul (modification impossible, suppression impossible)

3. nom=property(lambda self: self.__nom, None, None)        # Les None sont facultatifs

5. # Constructeur

6. def __init__(self, nom): self.__nom=nom

8. >>> p=personne("xxx")

9. >>> p.nom

10. 'xxx'

11. >>> p.nom="toto"

12. AttributeError: can’t set attribute

13. >>> del p.nom="toto"

14. AttributeError: can’t delete attribute

Une autre syntaxe possible est d’associer chaque élément (getter/setter/deleter) individuellement.

1. class personne:

2. # Écriture du getter et association immédiate

3. def __get_nom(self): return self.__nom

4. nom=property(__get_nom)

6. # Écriture du setter et association immédiate

7. def __set_nom(self, nom): self.__nom=nom

8. nom=nom.setter(__set_nom)

10. # Écriture du deleter et association immédiate

11. def __del_nom(self): del self.__nom

12. nom=nom.deleter(__del_nom)

14. # Constructeur

15. def __init__(self, nom): self.__nom=nom

Dans les trois cas, l’association se fera toujours sur l’élément que l’on veut montrer à l’extérieur. Accessoirement, rien n’interdit d’utiliser un autre token n’ayant aucune ressemblance syntaxique avec l’attribut interne (autrement dit, ce n’est pas parce que l’attribut interne se nomme __nom que l’attribut externe doit pour autant se nommer nom). Rien n’interdit même d’utiliser trois tokens totalement distincts pour les trois éléments getter/setter/deleter.

On peut raccourcir cette surcouche property() en utilisant le mot-clef @property avant d’écrire le getter. De là, on peut rajouter ensuite @token.setter pour le setter et @token.deleter pour le deleter.

1. class personne:

2. # Écriture du getter de façon immédiate

3. @property

4. def nom(self): return self.__nom

6. # Écriture du setter de façon immédiate

7. @nom.setter

8. def nom(self, nom): self.__nom=nom

10. # Écriture du deleter de façon immédiate

11. @nom.deleter

12. def nom(self): del self.__nom

14. # Constructeur

15. def __init__(self, nom): self.__nom=nom

Le @xxx est ce qu'on nomme communément un « sucre syntaxique » (syntaxe allégée permettant de simplifier certains termes). On le reverra dans d’autres conditions.

À noter : sous Python 2, la fonction property() n’est accessible que depuis un objet de base Python nommé object.

Dans cette version, il convient alors de faire hériter (la notion « d’héritage » sera vue ultérieurement) son propre objet de cet objet primitif. Cela se fait en mettant l'objet dont on veut hériter (donc ici object) en paramètre de son propre objet.

1. # Exemple Python 2

2. class personne(object):

3. # Écriture des fonctions dédiées

4. def __get_nom(self): return self.__nom

5. def __set_nom(self, nom): self.__nom=nom

6. def __del_nom(self): del self.__nom

8. # Encapsulation de ces fonctions dans la fonction property()

9. nom=property(__get_nom, __set_nom, __del_nom)

11. # Constructeur

12. def __init__(self, nom): self.__nom=nom

Sous Python 3, l’objet de base object est devenu implicitement hérité ce qui évite d'avoir à le faire manuellement.

XIV-6-c - Privatisation par convention

Une autre façon de créer des attributs privés est possible en faisant précéder l'attribut d'un simple underscore _).

1. class personne:

2. def __init__(self, nom):

3. self._nom=nom

4. print("Je crée l'objet %s avec un paramètre [%s] " % (self, nom))

6. >>> p=personne("xxx")

7. Je crée l’objet <__main__.personne instance at 0x7fc685a86290> avec un paramètre [moi]

8. >>> p

9. <__main__.personne instance at 0x7fc685a86290>

En réalité, cela ne protège l'attribut que de façon « conventionnelle » et non physique. En effet, le simple underscore n’a aucune signification pour Python (un tel attribut est alors public comme tout autre). Il est cependant universellement reconnu par les programmeurs. Bref, l’attribut reste public, mais l'ensemble des programmeurs accepte conventionnellement de ne pas y toucher (ou d’y toucher en connaissant les risques intrinsèques que cela peut entrainer sur la portabilité et l’évolutivité).

1. class personne:

2. def __init__(self, nom):

3. self._nom=nom

5. >>> p=personne("xxx")

6. >>> p._nom

7. 'xxx'

8. >>> p._nom="toto"

9. >>> p._nom

10. 'toto'

Bien que la terminologie ne soit pas exacte (elle correspond à une définition différente dans la méthodologie officielle objet), on nomme ces attributs des « attributs semi‑privés » ou « protégés » (protected).

XV - Compléments sur les classes

XV-1 - L’héritage

L’héritage de classes est un mécanisme qui permet à une classe Y de bénéficier des attributs et méthodes d’une classe X tout en pouvant se rajouter ses propres attributs et méthodes. Cela permet de définir facilement un arbre d’objets tout en centralisant les éléments communs.

Un bon résumé pour définir l’héritage est de dire « est un ». Ainsi, si on admet qu’une voiture est un véhicule, alors on peut concevoir un héritage entre un objet véhicule et un objet voiture.

La syntaxe d'un héritage se fait à la définition de l'objet où l’on rajoute, après son nom, le nom de l'objet hérité entre parenthèses. Dans le cas d’un héritage multiple (un objet héritant de plusieurs), alors on met les différents noms des objets séparés par des virgules.

Exemple : une gestion de véhicules pourra commencera par définir un objet véhicule pour l’affiner ensuite avec un objet voiture et un autre avion.

On commence alors par créer l'objet de base vehicule…

1. class vehicule:

2. def __init__(self, vitesse, roues):

3. self._vitesse=vitesse

4. self._roues=roues

6. def affich(self):

7. print(

8. "véhicule (%s) - vitesse=%d, roues=%d" % (

9. self,

10. self._vitesse,

11. self._roues,

12. )

13. )

Ensuite, on crée les objets voiture et avion qui héritent de cet objet vehicule.

1. class voiture(vehicule):

2. def __init__(self, motorisation, vitesse, roues):

3. vehicule.__init__(self, vitesse, roues)

4. self.__motorisation=motorisation

6. def affich(self):

7. vehicule.affich(self)

8. print(

9. "voiture (%s) - motorisation=%s, vitesse=%d, roues=%d" % (

10. self,

11. self.__motorisation,

12. self._vitesse,

13. self._roues,

14. )

15. )

17. class avion(vehicule):

18. def __init__(self, altitude, vitesse, roues):

19. super(avion, self).__init__(vitesse, roues)

20. self.__altitude=altitude

22. def affich(self):

23. super(avion, self).affich()

24. print(

25. "avion (%s) - altitude=%d, vitesse=%d, roues=%d" % (

26. self,

27. self.__altitude,

28. self._vitesse,

29. self._roues,

30. )

31. )

33. >>> v=voiture("diesel", 100, 4)

34. >>> v.affich()

35. véhicule (<__main__.voiture object at 0xb72308ac>) - vitesse=100, roues=4

36. voiture (<__main__.voiture object at 0xb72308ac>) - motorisation=diesel, vitesse=100, roues=4

37. >>> a=avion(25000, 200, 2)

38. >>> a.affich()

39. véhicule (<__main__.avion object at 0xb723090c>) - vitesse=200, roues=2

40. avion (<__main__.avion object at 0xb723090c>) - altitude=25000, vitesse=200, roues=2

Grâce à l’héritage, les objets voiture et avion récupèrent automatiquement les attributs de l’objet parent vehicule (vitesse et roues) qui peuvent alors être considérés et manipulés comme attributs au même titre que les autres.

L’héritage de classes s’étend bien entendu aussi aux éléments privés, mais comme ils sont privés, ils ne sont pas accessibles depuis la classe fille (sauf en donnant leur vrai nom obfusqué). D’où le simple underscore dans les attributs vitesse et roues de l’exemple pour que les classes filles puissent y accéder tout en indiquant aux autres développeurs que ces éléments doivent rester protégés).

De plus, l’héritage entraine une délégation des méthodes (la classe fille délègue certaines tâches à la classe mère ; telle la méthode __init__() de cet exemple qui fait appel à vehicule.__init__() pour initialiser les éléments de vehicule ou la méthode affich() qui fait appel à vehicule.affich() pour les afficher).

Dans la majorité des langages objets (C++, C#, php), cette délégation est implicite. En Python, elle sera implicite si la classe fille ne définit pas de méthode du même nom, mais devra être explicitement demandée dans le cas contraire (surcharge).

Celle-ci peut se faire de deux façons :

- appel explicite de l’objet et de la méthode dont on veut hériter. La syntaxe est alors la suivante : classe_mere.methode(instance de classe, paramètres de la methode). Dans l'exemple, c'est la façon dont l'objet voiture délègue l'initialisation et l'affichage à l'objet vehicule ;

- utilisation de la fonction super(). La syntaxe est alors la suivante : super(classe courante, instance de classe).methode(paramètres). Dans l'exemple, c'est la façon dont l'objet avion délègue l'initialisation et l'affichage à l'objet vehicule.

La différence entre les deux écritures est généralement minime. Elle ne devient réellement visible que dans les cas d’héritages complexes (tel un héritage en diamant où deux classes B et C héritent chacune de A ; et une quatrième classe D hérite de B et C en même temps) :

1. class A:

2. def __init__(self):

3. print("A.init")

5. class B(A):

6. def __init__(self):

7. print("B.init")

8. A.__init__(self)

10. class C(A):

11. def __init__(self):

12. print("C.init")

13. A.__init__(self)

15. class D(B, C):

16. def __init__(self):

17. print("D.init")

18. B.__init__(self)

19. C.__init__(self)

21. >>> D()

22. D.init

23. B.init

24. A.init

25. C.init

26. A.init

1. class A:

2. def __init__(self):

3. print("A.init")

5. class B(A):

6. def __init__(self):

7. print("B.init")

8. super(B, self).__init__()

10. class C(A):

11. def __init__(self):

12. print("C.init")

13. super(C, self).__init__()

15. class D(B, C):

16. def __init__(self):

17. print("D.init")

18. super(D, self).__init__()

20. >>> D()

21. D.init

22. B.init

23. C.init

24. A.init

Avec la syntaxe classique, la méthode __init__() de la classe A est appelée deux fois (une fois depuis la classe B et une autre depuis la classe C). Avec la syntaxe passant par la fonction super(), la méthode __init__() de la classe A n’est appelée qu’une seule fois via le « MRO » (Method Resolution Order) de Python.

À noter : puisque les paramètres de la fonction super() respectent toujours le même schéma (classe courante, instance courante), elle a été simplifiée dans Python 3 pour pouvoir être appelée sans paramètres ; ceux-ci étant alors pris par défaut comme étant le nom et l’instance de la classe qui invoque la fonction.

1. class A:

2. def __init__(self, mot):

3. print("a", mot)

5. class B(A):

6. def __init__( self, mot):

7. print("b")

8. super(B, self).__init__(mot)                    # Ancienne syntaxe

10. class C(A):

11. def __init__( self, mot):

12. print("c")

13. super(C, self).__init__(mot)                    # Ancienne syntaxe

15. class D(A):

16. def __init__(self, mot):

17. print("d")

18. super().__init__(mot)                           # Nouvelle syntaxe

20. class E(B, C, D):

21. def __init__(self, mot):

22. print("e")

23. super().__init__(mot)                           # Nouvelle syntaxe

25. >>> E("xxx")

26. e

27. b

28. c

29. d

30. a xxx

Cela apporte un avantage non négligeable quand une classe privée veut hériter d'un objet public. La première syntaxe oblige à passer par le nom obfusqué…

1. class A:

2. def __init__(self): print("A: %s" % self)

4. class B:

5. class __private(A):

6. def __init__(self):

7. super(B._B__private, self).__init__()

8. print("__private: %s" % self)

10. def __init__(self):

11. B.__private()

12. print("B: %s" % self)

14. >>> B()

15. A: <__main__.B.__private object at 0x7f52803002e8>

16. __private: <__main__.B.__private object at 0x7f52803002e8>

17. B: <__main__.B object at 0x7f52802f6dd8>

… tandis que la nouvelle syntaxe laisse Python gérer tout seul.

1. class A:

2. def __init__(self): print("A: %s" %self)

4. class B:

5. class __private(A):

6. def __init__(self):

7. super().__init__()

8. print("__private: %s" %self)

10. def __init__(self):

11. B.__private()

12. print("B: %s" %self self)

14. >>> B()

15. A: <__main__.B.__private object at 0x7f52803002e8>

16. __private: <__main__.B.__private object at 0x7f52803002e8>

17. B: <__main__.B object at 0x7f52802f6dd8>

XV-2 - L’héritage face à l’évolution

Il a été vu au chapitre précédent que la délégation des méthodes (la classe fille délègue certaines tâches à la classe mère) n’était pas implicite. Ce qui peut poser des soucis liés à l’évolution de la classe mère.

Reprenons notre objet vehicule contenant un attribut « vitesse maximale »…

1. class vehicule:

2. def __init__(self, vMax):

3. self.vMax=vMax

Celui qui voudra en hériter, quoi qu’il mette dans son propre objet, devra quand même prendre en considération cette valeur vMax. Exemple avec un objet voiture contenant un attribut marque…

1. class voiture(vehicule):

2. def __init__(self, marque, vMax):

3. super().__init__(vMax)

4. self.vmarque=marque

6. >>> v=voiture("Lada", 10)

7. >>> print(v.marque)

8. Lada

9. >>> print(v.vMax)

10. 10

Si demain la classe vehicule évolue pour s’enrichir d’un attribut vMin, tous ses héritiers devront évoluer de la même façon, et de même pour ceux qui hériteront des objets eux‑mêmes hérités. Très rapidement cette contrainte deviendra ingérable et l’héritage ne sera plus utilisé.

Une solution pourra être utilisée à base de valeurs par défaut, mais cette solution ne sera que bancale (tout le monde n’est pas obligé de vouloir mettre une valeur bidon dans son objet quand l’appelant ne met pas ce qu’il faut).

La bonne façon pour régler ce souci est d’utiliser les arguments spéciaux *args et **kwargs. L’utilisateur qui veut hériter d’un objet particulier pourra d’abord récupérer ses propres arguments puis n’aura ensuite qu’à passer ces arguments spéciaux à l’objet parent.

Reprenons notre héritage…

1. class voiture(vehicule):

2. def __init__(self, marque, *args, **kwargs):

3. super().__init__(*args, **kwargs)

4. self.vmarque=marque

6. >>> v=voiture("Lada", 10)

7. >>> print(v.marque)

8. Lada

9. >>> print(v.vMax)

10. 10

Cela ne change pas le fait que si demain l’objet vehicule évolue, l’utilisateur qui l’utilise par délégation devra évoluer avec, mais le programmeur de l’objet voiture, lui, n’a rien à faire, son objet s’adaptera automatiquement aux évolutions de son ancêtre.

1. # Objet "vehicule" ayant évolué

2. class vehicule:

3. def __init__(self, vMin, vMax):

4. self.vMin=vMin

5. self.vMax=vMax

7. # Objet "voiture " restant inchangé

8. class voiture(vehicule):

9. def __init__(self, marque, *args, **kwargs):

10. super().__init__(*args, **kwargs)

11. self.vmarque=marque

13. # Seul l’appelant (l’utilisateur) doit s’adapter

14. >>> v=voiture("Lada", 0, 10)

15. >>> print(v.marque)

16. Lada

17. >>> print(v.vMin)

18. 0

19. >>> print(v.vMax)

20. 10

XV-3 - Les attributs et méthodes statiques

Les attributs et méthodes vus jusqu’à présent s’appliquaient tous aux instances de classes. En effet, pour deux instances distinctes, les valeurs d’attributs ne sont pas forcément les mêmes (deux personnes distinctes n’ont pas forcément le même nom) et les méthodes (qui généralement utilisent les attributs) prennent alors fatalement en compte la valeur des attributs en question, valeur qui diffère alors selon les instances créées.

Toutefois, certains attributs et méthodes peuvent s'appliquer à la classe elle‑même et non à ses instances. Dans ce cas, il peut être alors judicieux de les définir en tant qu'attributs ou méthodes statiques (appelées aussi « attributs ou méthodes de classe »).

XV-3-a - Attribut statique

Pour l'attribut c'est implicite dès lors qu'il est défini en dehors de toute méthode. À partir de là, depuis l'extérieur, l’accès à cet attribut se fera depuis le nom de la classe elle‑même.

Exemple : création d’un compteur d’instances

1. class personne:

2. cpt=0                                  # Compteur de personnes (membre statique)

3. def __init__(self, nom):

4. self.__nom=nom

5. personne.cpt+=1                    # Une personne de plus

6. def __del__(self):

7. personne.cpt-=1                    # Une personne de moins

9. >>> p1=personne("Pierre")

10. >>> p2=personne("Paul")

11. >>> p3=personne("Jacques")

12. >>> personne.cpt

13. 3

14. >>> del p3

15. >>> personne.cpt

16. 2

Bien que l’accès à l’attribut se fasse depuis le nom de la classe elle‑même, il peut aussi se faire depuis une instance de la classe et se comporte alors comme un « attribut d’instance » tout en restant attribut de classe (donc unique dans la classe).

1. class toto: item=123

3. >>> toto.item

4. 123

5. >>> p1=toto()

6. >>> p1.item

7. 123

8. >>> p2=toto()

9. >>> p2.item

10. 123

11. >>> toto.item=456

12. >>> toto.item

13. 456

14. >>> p2.item

15. 456

16. >>> p1.item

17. 456

18. >>> p3=toto()

19. >>> p3.item

20. 456

L’attribut de classe peut toutefois devenir un vrai attribut d’instance si on le modifie (recrée) dans l’instance. Tout comme pour les variables globales et locales, l’attribut d’instance prend alors le pas sur l’attribut de classe.

1. >>> class toto: item=123

2. ...

3. >>> p1=toto()

4. >>> p2=toto()

5. >>> toto.item

6. 123

7. >>> p1.item

8. 123

9. >>> p2.item

10. 123

11. >>> p1.item=456

12. >>> p1.item

13. 456

14. >>> p2.item

15. 123

16. >>> toto.item

17. 123

XV-3-b - Méthode statique

Pour la méthode, cela se fait en l’encapsulant dans une surcouche staticmethod(). À ce moment‑là, la méthode ne recevra plus d’instance de classe dans ses arguments et n’a donc plus à définir de self.

1. class point:

2. def __milieu(x, y):

3. return (x.__v + y.__v) / 2

4. milieu1=staticmethod(__milieu)                              # Méthode statique

5. milieu2=staticmethod(lambda x, y : (x.__v + y.__v) / 2)     # Méthode statique

6. def __init__(self, v):

7. self.__v=v

9. >>> p1=point(4)

10. >>> p2=point(8)

11. >>> point.milieu1(p1, p2)

12. 6.0

13. >>> point.milieu2(p1, p2)

14. 6.0

Comme on le voit, les méthodes milieu1() ou milieu2(), permettant de calculer le milieu barycentrique de deux points, ne peuvent pas s’appliquer à un point en particulier. Elles ne peuvent donc pas être utilisées comme des méthodes d’instance. Cependant, elles ne concernent que des points et s’appliquent donc à tous les points en général et peuvent par conséquent être intégrées dans l’objet point (ce qui renforce la modularité du code). La solution est d’en faire alors des « méthodes statiques ».

À noter : la méthode interne __milieu() étant définie dans la classe, elle bénéficie quand même d’un accès à tous les attributs de la classe, y compris ses attributs privés (elle reçoit en paramètre deux instances de point x et y, elle a alors accès à x.__v et y.__v). Et les méthodes statiques milieu1() et milieu2() ont aussi cette possibilité.

Et de même que pour les getters/setters/deleters, il existe un sucre syntaxique @staticmethod qui permet de raccourcir cette surcouche staticmethod().

1. class point:

2. @staticmethod                            # Préparation à la méthode de classe

3. def milieu(x, y):                        # Définition de la méthode de classe

4. return (x.__v + y.__v) / 2

5. def __init__(self, v):

6. self.__v=v

8. >>> p1=point(4)

9. >>> p2=point(8)

10. >>> point.milieu(p1, p2)

11. 6.0

XV-4 - La fonction « classmethod() »

La fonction classmethod() permet de transformer une méthode pour lui faire recevoir en premier paramètre non plus l’instance, mais la classe qui l’évoque.

Prenons un exemple simple : un objet permettant de manipuler une date (jour, mois, année)…

1. class date:

2. def __init__(self, jj, mm, aa):

3. self.jj=jj

4. self.mm=mm

5. self.aa=aa

7. >>> a=date(1, 1, 2000)

8. >>> print(a.jj, a.mm, a.aa)

9. 1 1 2000

L’utilisateur qui veut utiliser cet objet doit alors l’instancier selon une convention bien définie en lui passant les trois caractéristiques attendues (jour, mois et année) en argument.

Si maintenant l’utilisateur reçoit une date sous forme de chaîne ("jj/mm/aaaa"), il doit alors l’éclater pour pouvoir récupérer ses divers éléments avant de pouvoir créer son objet.

Si ce comportement se généralise, il peut alors avoir envie de modifier son objet pour qu’il puisse absorber ou une date classique ou une date chaîne. Petit travail facile de factorisation qui, en retour, lui simplifiera ensuite grandement l’écriture du code qui utilisera cet objet.

Une première solution sera alors de modifier __init__() pour l’autoriser à absorber les deux types de paramètres. Faisable, mais si demain un 3° format différent se présente, et un 4° après-demain…?

Une seconde solution sera de créer une méthode statique renvoyant un nouvel objet date :

1. class date:

2. def __init__(self, jj, mm, aa):

3. self.jj=jj

4. self.mm=mm

5. self.aa=aa

6. @staticmethod

7. def fromString(string):

8. (jj, mm, aa)=string.split("/")

9. return date(jj, mm, aa)

11. >>> a=date.fromString("1/1/2000")

12. >>> print(a.jj, a.mm, a.aa)

13. 1 1 2000

L’avantage c’est que si un troisième format d’entrée arrive demain, on pourra toujours rajouter une autre méthode fromXXX, fromYYY et ainsi de suite.

L’inconvénient c'est que la méthode utilise le nom de l’objet de façon littérale. Déjà cela induit des contraintes d’évolutivité (si le nom change demain…) et surtout cela peut poser des soucis liés à l’héritage (la méthode renvoie un objet date alors que l’utilisateur pourrait avoir envie d’un objet hérité).

1. class date:

2. def __init__(self, jj, mm, aa):

3. self.jj=jj

4. self.mm=mm

5. self.aa=aa

6. @staticmethod

7. def fromString(string):

8. return date(*string.split("/"))         # Remarquez l'utilisation de l'étoile (unpacking)…

10. class subDate(date): pass

12. >>> a=date.fromString("1/1/2000")

13. >>> type(a)

14. <class '__main__.date'>

16. >>> b=subDate.fromString("1/1/3000")

17. >>> type(b)

18. <class '__main__.date'>                         # Il voulait créer un subDate !!!

La bonne solution sera d’utiliser alors une méthode encapsulée dans une surcouche classmethod(). Cette méthode recevra alors en premier paramètre le nom de la classe qui l’invoque (généralement on stocke ce nom dans un paramètre conventionnellement nommé cls). L’utilisateur pourra alors utiliser ce nom de classe pour générer l’objet qui en découle.

1. class date:

2. def __init__(self, jj, mm, aa):

3. self.jj=jj

4. self.mm=mm

5. self.aa=aa

6. def __fromString(cls, string):

7. return cls(*string.split("/"))

8. fromString=classmethod(__fromString)

10. class subDate(date): pass

12. >>> a=date.fromString("1/1/2000")

13. >>> type(a)

14. <class '__main__.date'>

16. >>> b=subDate.fromString("1/1/3000")

17. >>> type(b)

18. <class '__main__.subDate'>                      # Il a bien son objet subDate !!!

Cette solution répond aux mêmes contraintes que la solution précédente, mais offre l’avantage de suivre l’évolution du code (si le nom de l’objet change, cela se fera de façon transparente sans avoir besoin de revérifier tout son contenu), ainsi que les contraintes liées à l'héritage.

Et de même que pour les getters/setters/deleters ou pour la méthode statique, le sucre syntaxique @classmethod est lui aussi autorisé.

1. class date:

2. def __init__(self, jj, mm, aa):

3. self.jj=jj

4. self.mm=mm

5. self.aa=aa

6. @classmethod

7. def fromString(cls, string):

8. return cls(*string.split("/"))

10. >>> a=date.fromString("1/1/2000")

11. >>> type(a)

12. <class '__main__.date'>

XV-5 - Gestion interne des attributs

Dans ce chapitre, tous les attributs et méthodes décrits sont automatiquement créés par Python pour tout objet.

XV-5-a - L’attribut __dict__

L’attribut __dict__ référence tous les attributs créés dans l’instance.

1. class xxx:

2. def __init__(self):

3. self.s="Hello"

4. self.i=123

6. >>> x=xxx()

7. >>> print(x.__dict__)

8. {'s': 'Hello', 'i': 123}

Tout nouvel attribut viendra compléter cet attribut interne…

1. >>> x.f=3.1416

2. >>> print(x.__dict__)

3. {'s': 'Hello', 'i': 123, 'f': 3.1416}

De même, tout remplissage manuel de cet attribut équivaudra à la création (ou la modification) de l’attribut correspondant…

1. >>> x.__dict__["i"]=456

2. >>> print(x.i)

3. 456

XV-5-b - La méthode __delattr__()

La méthode __delattr__() est automatiquement appelée à chaque fois qu’un attribut est supprimé de son instance.

1. class xxx:

2. def __init__(self):

3. self.s="Hello"

4. self.i=123

5. def __delattr__(self, attr):

6. print("deleting [%s]" % attr)

7. super().__delattr__(attr)

9. >>> x=xxx()

10. >>> print(x.i)

11. 123

12. >>> del x.i

13. deleting [i]

14. >>> print(x.i)

15. Traceback (most recent call last):

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

17. AttributeError: 'xxx' object has no attribute 'i'

Toutefois, la méthode n’est pas invoquée si l’attribut est supprimé par le __dict__.

1. >>> del x.__dict__["s"]

2. >>> print(x.s)

3. Traceback (most recent call last):

4. Traceback (most recent call last):

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

6. AttributeError: 'xxx' object has no attribute 's'

XV-5-c - La méthode __getattr__()

La méthode __getattr__() est automatiquement appelée à chaque fois qu’une instance invoque un attribut inexistant.

En revanche, pour des raisons de performances, si l’attribut existe, alors la méthode n’est pas appelée.

class xxx:
def __init__(self):
self.s="Hello"
self.i=123
def __getattr__(self, attr):
print("getting [%s]" % attr)
return "perdu"

>>> x=xxx()
>>> print(x.s)
Hello
>>> print(x.i)
123
>>> print(x.j)
getting [j]
Perdu

XV-5-d - La méthode __getattribute__()

La méthode __getattribute__() est automatiquement appelée à chaque fois qu’un attribut est invoqué par son instance.

1. class xxx:

2. def __init__(self):

3. self.s="Hello"

4. self.i=123

5. def __getattribute__(self, attr):

6. print("getting [%s]" % attr)

7. return super().__getattribute__(attr)

9. >>> x=xxx()

10. >>> print(x.i)

11. getting [i]

12. 123

Contrairement à ce qui se passe dans la méthode __delattr__(), ici, passer par le __dict__ pour tenter de contourner cette méthode en y cherchant directement l’attribut implique quand même un accès à l’attribut __dict__ ce qui provoque alors tout de même l’appel de la méthode.

1. >>> print(x.__dict__["i"])

2. getting [__dict__]

3. 123

XV-5-e - La méthode __setattr__()

La méthode __setattr__() est automatiquement appelée à chaque fois qu’un attribut est créé ou modifié par son instance.

1. class xxx:

2. def __init__(self):

3. self.s="Hello"

4. self.i=123

5. def __setattr__(self, attr, value):

6. print("setting [%s]: (%d)" % (attr, value))

7. super().__setattr__(attr, value)

9. >>> x=xxx()

10. setting [s]: (Hello)

11. setting [i]: (123)

12. >>> print(x.i)

13. 123

14. >>> x.i=456

15. setting [i]: (456)

16. >>> print(x.i)

17. 456

Cependant, la méthode n’est pas invoquée si l’attribut est créé ou modifié par le __dict__ et la modification du __dict__ n’entraine pas non plus l’appel de cette méthode.

1. >>> x.__dict__["i"]=789

2. >>> print(x.i)

3. 789

XV-5-f - La méthode __new__()

En Python, la méthode spéciale __init__() est souvent appelée constructeur de l’objet. Il s’agit en fait d’un abus de langage : la méthode __init__() ne construit pas l’objet, elle intervient après la création de ce dernier pour l’initialiser.

Le vrai constructeur d’une classe est la méthode __new__(). Cette méthode est une méthode statique (un cas particulier qui n’a pas besoin d’être déclaré comme tel) qui prend une classe en premier paramètre (le paramètre self n’existe pas encore puisque l’objet n’est pas encore créé), et doit renvoyer l’objet nouvellement créé. Les autres paramètres sont identiques à ceux reçus par __init__() puisque la méthode doit ensuite appeler cette dernière en lui passant les paramètres qu’elle attend (ce qui se fait alors de façon automatique).

1. class cercle:

2. def __new__(cls, rayon):

3. print("new [%s], rayon=%d" % (cls, rayon))

4. return super().__new__(cls)

5. def __init__(self, rayon):

6. print("init [%s], rayon=%d" % (self, rayon))

8. >>> c=cercle(5)

9. new [<class '__main__.cercle'>], rayon=5

10. init [<__main__.cercle object at 0x7feff77427b8>], rayon=5

11. >>> print(c)

12. <__main__.cercle object at 0x7feff77427b8>

Cette méthode est particulièrement utile pour les objets immuables (str, tuple). En effet, il est impossible d’agir sur les objets dans la méthode __init__(), puisque celle-ci intervient après la création de l’objet, et que de fait, l’objet créé n’est alors plus modifiable.

Si l’on souhaite hériter d’un type immuable et agir sur l’initialisation de l’objet, il est donc nécessaire de redéfinir __new__(). Par exemple, pour créer une classe point qui hériterait de tuple…

1. class point(tuple):

2. def __new__(cls, x, y):

3. print("new [%s], x=%s, y=%s" % (cls, x, y))

4. return super().__new__(cls, (x, y))

5. x=property(lambda self: self[0])

6. y=property(lambda self: self[1])

8. >>> t=point(2, 3)

9. new [<class '__main__.point'>], x=2, y=3

10. >>> print(t[0])

11. 2

12. >>> print(t.x)

13. 2

14. >>> print(t[1])

15. 3

16. >>> print(t.y)

17. 3

… tout en gardant le bénéfice de l’immuabilité du tuple.

1. >>> t.x=123

2. Traceback (most recent call last):

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

4. AttributeError: can't set attribute

XV-5-g - La méthode __init_subclass__()

En Python, la méthode spéciale __init_subclass__() est appelée par un objet quand il est instancié par l’un de ses héritiers.

C’est une méthode là aussi statique qui prend également une classe en premier paramètre.

Elle permet de créer si besoin des attributs statiques dans la classe fille en les créant « à la volée » dans la classe mère.

Prenons un exemple simple : une gestion de figures géométriques avec nombre de sommets.

Première solution : des classes simples…

1. class carre:

2. sommets=4

3. def __init__(self, cote):

4. print("carre init: %s (cote=%d)" % (self, cote))

5. self.cote=cote

7. class pentagone:

8. sommets=5

9. def __init__(self, cote):

10. print("pentagone init: %s (cote=%d)" % (self, cote))

11. self.cote=cote

13. >>> c=carre(10)

14. carre init: <__main__.carre object at 0x7f02cc573470> (cote=10)

15. >>> c.cote

16. 10

17. >>> c.sommets

18. 4

19. >>> p=pentagone(12)

20. pentagone init: <__main__.pentagone object at 0x7f02cc573438> (cote=12)

21. >>> p.cote

22. 12

23. >>> p.sommets

24. 5

C’est dommage qu’il n’y ait pas quelque part une espèce de centralisation des éléments redondants (ici le nombre de sommets).

Donc seconde solution : des classes qui héritent d’une classe mère en lui passant les sommets qu’elle a à gérer…

1. class figure:

2. def __init__(self, sommets):

3. print("figure init: %s (sommets=%d)" % (self, sommets))

4. self.sommets=sommets

6. class carre(figure):

7. def __init__(self, cote):

8. super().__init__(4)

9. print("carre init: %s (cote=%d)" % (self, cote))

10. self.cote=cote

12. class pentagone(figure):

13. def __init__(self, cote):

14. super().__init__(5)

15. print("pentagone init: %s (cote=%d)" % (self, cote))

16. self.cote=cote

18. >>> c=carre(10)

19. figure init: <__main__.carre object at 0x7fcf87fea438> (sommets=4)

20. carre init: <__main__.carre object at 0x7fcf87fea438> (cote=10)

21. >>> c.cote

22. 10

23. >>> c.sommets

24. 4

25. >>> p=pentagone(12)

26. figure init: <__main__.pentagone object at 0x7fcf87fbcb38> (sommets=5)

27. pentagone init: <__main__.pentagone object at 0x7fcf87fbcb38> (cote=12)

28. >>> p.cote

29. 12

30. >>> p.sommets

31. 5

C’est fonctionnel, mais les sommets d’une figure n’ayant pas vocation à changer au cours du temps, c’est dommage de ne pas les stocker de façon statique.

D’où la 3° méthode : passer par __init_subclass__() qui aura pour rôle de paramétrer l’attribut statique de la classe mère en fonction de la configuration de la classe fille.

1. class figure:

2. def __init__(self):

3. print("figure init: %s" % self)

5. def __init_subclass__(cls, sommets):

6. super().__init_subclass__()

7. print("figure init_subclass: %s (sommets=%d)" % (cls, sommets))

8. cls.sommets=sommets

10. >>> class carre(figure, sommets=4):

11. ...    def __init__(self, cote):

12. ...        super().__init__()

13. ...        print("carre init: %s (cote=%d)" % (self, cote))

14. ...        self.cote=cote

15. ...

16. figure init_subclass: <class '__main__.carre'> (sommets=4)

18. >>> class pentagone(figure, sommets=5):

19. ...    def __init__(self, cote):

20. ...        super().__init__()

21. ...        print("pentagone init: %s (cote=%d)" % (self, cote))

22. ...        self.cote=cote

23. ...

24. figure init_subclass: <class '__main__.pentagone'> (sommets=5)

26. >>> c=carre(10)

27. figure init: <__main__.carre object at 0x7fd04a525b70>

28. carre init: <__main__.carre object at 0x7fd04a525b70> (cote=10)

29. >>> print(c.cote)

30. 10

31. >>> print(c.sommets)

32. 4

33. >>> p=pentagone(12)

34. figure init: <__main__.pentagone object at 0x7fd04a525b00>

35. pentagone init: <__main__.pentagone object at 0x7fd04a525b00> (cote=12)

36. >>> print(p.cote)

37. 12

38. >>> print(p.sommets)

39. 5

C’est un peu le principe en allégé de la métaclasse que l’on verra un peu plus tard.

XV-5-h - L’attribut __slots__

Il a été vu dans le chapitre sur les objets qu’il est possible de leur créer de nouveaux attributs de façon manuelle et externe…

1. class personne:

2. def __init__(self, nom):

3. self.nom=nom

5. >>> p=personne("LeRoi")

6. >>> p.prenom="Arthur"

7. >>> p.nom

8. 'LeRoi'

9. >>> p.prenom

10. 'Arthur'

… ce qui est rarement une bonne chose…

1. >>> p1=personne("LeRoi")

2. >>> p1.prenom="Arthur"

3. >>> p2=personne("Autre")

4. >>>

5. >>> for x in (p1, p2):

6. ...    print(x.nom, x.prenom)

7. ...

8. LeRoi, Arthur

9. Traceback (most recent call last):

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

11. AttributeError: personne' object has no attribute 'prenom'

L’attribut __slots__ (attribut statique) permet de protéger son objet contre de telles manipulations.

Il suffit de le créer dans son objet en lui donnant l’attribut ou la liste des attributs permis pour l’objet. Toute création d’un attribut non présent dans cette liste provoquera une exception.

1. class personne:

2. __slots__=("nom", "prenom")

4. def __init__(self, nom, prenom):

5. self.nom=nom

6. self.prenom=prenom

8. >>> p=personne("LeRoi", "Arthur")

9. >>> p.age=800

10. Traceback (most recent call last):

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

12. AttributeError: 'personne' object has no attribute 'age'

Ce verrouillage se fait au travers de l’attribut __dict__ qui est alors purement supprimé de l’objet.

1. >>> print(p.__dict__)

2. Traceback (most recent call last):

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

4. AttributeError: 'personne' object has no attribute '__dict__'

XV-6 - Trucs et astuces divers sur les classes

XV-6-a - Rendre une classe « callable »

Une classe peut devenir « callable » ; c’est-à-dire qu’une instance de cette classe peut être appelée en tant que fonction ; par le biais de la méthode spécifique __call__().

1. class ccc:

2. def __call__(self, x, y):

3. print("ici (%s, %s)" % (x, y))

5. >>> x=ccc()                    # Création d’un objet "ccc" instancié dans une variable "x"

6. >>> x(2, 3)                    # Appel de "x" comme si c’était une fonction

7. ici (2, 3)

XV-6-b - Rendre une classe « indexable »

Une classe peut devenir « indexable » ; c’est-à-dire être utilisée comme si elle était une séquence (tuple, liste ou dictionnaire) dans laquelle on accèderait à un élément indexé ou slicé ; par le biais des méthodes spécifiques __getitem__() (pour récupérer un élément) et __setitem__() (pour positionner un élément).

Cela se pratique généralement quand la classe contient un attribut de nature séquentiel et qu’on désire accéder aux éléments distincts de l’attribut sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé).

1. class maListe:

2. def __init__(self, tab=None):

3. self.__tab=list(tab or ())

4. def __getitem__(self, idx):

5. print("getitem (%d)" % idx)

6. return self.__tab[idx]

7. def __setitem__(self, idx, n):

8. print("setitem (%d, %s)" % (idx, n))

9. self.__tab[idx]=n

11. >>> m=maListe(("Pim", "Pam", "Poum"))

12. >>> print(m[2])

13. getitem (2)

14. Poum

15. >>> m[2]="toto"

16. setitem (2, toto)

17. >>> print(m[2])

18. toto

XV-6-c - Rendre une classe « itérable »

Une classe peut devenir « itérable » ; c’est-à-dire être utilisable dans une boucle d’itération ; par le biais de la méthode spécifique __iter__().

Cela se pratique généralement quand la classe contient un attribut de nature séquentielle et qu’on désire itérer directement sur les éléments distincts de l’attribut sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé) ni avoir besoin de passer par son indexation.

1. class maListe:

2. def __init__(self, tab=None):

3. self.__tab=list(tab or ())

4. def __iter__(self):

5. yield from self.__tab                # for x in self.__tab: yield x (en Python 2)

7. >>> m=maListe(("Pim", "Pam", "Poum"))

8. >>> for x in m : print(x)

9. Pim

10. Pam

11. Poum

12. >>> print(tuple(m))

13. ('Pim', 'Pam', 'Poum')

Une telle classe qui se veut itérable peut aussi implémenter la méthode spécifique __next__() permettant alors d’y appliquer la fonction next().

1. class maListe:

2. def __init__(self, tab=None):

3. self.__tab=list(tab or ())

4. self.__idx=0

5. def __next__(self):

6. if self.__idx == len(self.__tab):

7. raise StopIteration

8. t=self.__tab[self.__idx]

9. self.__idx+=1

10. return t

12. >>> m=maListe(("Pim", "Pam", "Poum"))

13. >>> while True: print(next(m))

14. ...

15. Pim

16. Pam,

17. Poum

18. Traceback (most recent call last):

19. File "<stdin>", line 13, in <module>

20. while True: print(next(m))

21. File "<stdin>", line 7, in __next__

22. raise StopIteration

23. StopIteration

XV-6-d - Rendre une classe « mesurable »

Une classe peut devenir « mesurable » ; c’est-à-dire indiquer sa longueur ; par le biais de la méthode spécifique __len__().

Cela se pratique généralement quand la classe contient un attribut de nature séquentielle et qu’on désire avoir la longueur de cet attribut sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé) ni avoir besoin de passer par son indexation.

1. class maListe:

2. def __init__(self, tab=None):

3. self.__tab=list(tab or ())

4. def __len__(self):

5. return len(self.__tab)

7. >>> m=maListe(("Pim", "Pam", "Poum"))

8. >>> print(len(m))

9. 3

XV-6-e - Indiquer une contenance dans une classe

Une classe peut indiquer une contenance ; c’est-à-dire vérifier si un élément est présent ou pas ; par le biais de la méthode spécifique __contains__().

Cela se pratique généralement quand la classe contient un attribut de nature séquentielle et qu’on désire simplement vérifier si cet attribut contient un élément particulier sans passer par son nom (surtout si cet attribut séquentiel est un attribut privé) ni avoir besoin de passer par son indexation ou son itération.

1. class maListe:

2. def __init__(self, tab=None):

3. self.__tab=list(tab or ())

4. def __contains__(self, x):

5. return x in self.__tab

7. >>> m=maListe(("Pim", "Pam", "Poum"))

8. >>> print("Poum" in m)

9. True

XV-6-f - Définir les opérateurs usuels

Une classe peut définir des opérateurs usuels (comme l’addition, soustraction, etc.) permettant ensuite de l’utiliser dans des opérations classiques.

1. class cPoint:

2. def __init__(self, x=0, y=0):

3. self.__x=x

4. self.__y=y

5. def __add__(self, n):                      # Redéfinit l’addition

6. return cPoint(self.__x + n.__x, self.__y + n.__y)

7. def __repr__(self):                        # Redéfinit la façon de représenter un objet

8. return "x=%d, y=%d" % (self.__x, self.__y)

10. >>> a=cPoint(2, 3)

11. >>> b=cPoint(4, 5)

12. >>> a+b

13. x=6, y=8

Voici une liste non exhaustive des opérateurs les plus usuels :

- mathématiques :

- __add__ (addition) : permet d’appeler objet+n

- __radd__ (addition renversée) : permet d’appeler n+objet,

- __iadd__ (addition inplace) : permet d’appeler objet+=n (si cette méthode n’est pas présente, ce sera __add__ qui sera utilisée à la place),

- __sub__ (soustraction) : permet d’appeler objet‑n

- __rsub__ (soustraction renversée) : permet d’appeler n‑objet,

- __isub__ (soustraction inplace) : permet d’appeler objet‑=n (si cette méthode n’est pas présente, ce sera __sub__ qui sera utilisée à la place),

- __mul__ (multiplication) : permet d’appeler objet*n,

- __rmul__ (multiplication renversée) : permet d’appeler n*objet,

- __imul__ (multiplication inplace) : permet d’appeler objet*=n (si cette méthode n’est pas présente, ce sera __mul__ qui sera utilisée à la place),

- __div__ (division dans Python 2, n’existe pas dans Python 3) : permet d’appeler objet/n,

- __rdiv__ (division renversée dans Python 2, n’existe pas dans Python 3) : permet d’appeler n/objet,

- __idiv__ (division inplace renversée dans Python 2, n’existe pas dans Python 3) : permet d’appeler objet/=n (si cette méthode n’est pas présente, ce sera __div__ qui sera utilisée à la place),

- __floordiv__ (division entière) : permet d’appeler objet//n,

- __rfloordiv__ (division entière renversée) : permet d’appeler n//objet,

- __ifloordiv__ (division entière inplace) : permet d’appeler objet//=n (si cette méthode n’est pas présente, ce sera __floordiv__ qui sera utilisée à la place),

- __truediv__ (division exacte dans Python 3, n’existe pas dans Python 2) : permet d’appeler objet/n,

- __rtruediv__ (division exacte renversée dans Python 3, n’existe pas dans Python 2) : permet d’appeler n/objet,

- __itruediv__ (division exacte inplace dans Python 3, n’existe pas dans Python 2) : permet d’appeler objet/=n (si cette méthode n’est pas présente, ce sera __truediv__ qui sera utilisée à la place),

- __mod__ (modulo) : permet d’appeler objet%n,

- __rmod__ (modulo renversé) : permet d’appeler n%objet,

- __imod__ (modulo inplace) : permet d’appeler objet%=n (si cette méthode n’est pas présente, ce sera __mod__ qui sera utilisée à la place),

- __pow__ (puissance) : permet d’appeler objet**n,

- __rpow__ (puissance renversée) : permet d’appeler n**objet,

- __ipow__ (puissance inplace) : permet d’appeler objet**=n (si cette méthode n’est pas présente, ce sera __pow__ qui sera utilisée à la place),

- __and__ (« et » bit à bit) : permet d’appeler objet&n,

- __rand__ (« et » bit à bit renversé) : permet d’appeler n&objet,

- __iand__ (« et » bit à bit inplace) : permet d’appeler objet&=n (si cette méthode n’est pas présente, ce sera __and__ qui sera utilisée à la place),

- __or__ (« ou » bit à bit) : permet d’appeler objet|n,

- __ror__ (« ou » bit à bit renversé) : permet d’appeler n|objet,

- __ior__ (« ou » bit à bit inplace) : permet d’appeler objet|=n (si cette méthode n’est pas présente, ce sera __or__ qui sera utilisée à la place),

- __xor__ (« ou exclusif » bit à bit) : permet d’appeler objet^n,

- __rxor__ (« ou » bit à bit renversé) : permet d’appeler n^objet,

- __ixor__ (« ou » bit à bit inplace) : permet d’appeler objet^=n (si cette méthode n’est pas présente, ce sera __xor__ qui sera utilisée à la place),

- __lshift__ (décalage vers la gauche) : permet d’appeler objet << n,

- __ilshift__ (décalage vers la gauche inplace) : permet d’appeler objet<<=n (si cette méthode n’est pas présente, ce sera __lshift__ qui sera utilisée à la place),

- __rshift__ (décalage vers la droite) : permet d’appeler objet >> n,

- __irshift__ (décalage vers la droite inplace) : permet d’appeler objet>>=n (si cette méthode n’est pas présente, ce sera __rshift__ qui sera utilisée à la place),

- __neg__ (passage en négatif) : permet d’appeler ‑objet,

- __pos__ (identité neutre) : permet d’appeler +objet,

- __inv__ (inversion bit à bit) : permet d’appeler ~objet,

- __abs__ (valeur absolue) ;

- booléens : __eq__ (égal à), __ne__ (différent de), __lt__ (inférieur à), __le__ (inférieur ou égal à), __gt__ (supérieur à), __ge__ (supérieur ou égal à), et . À noter sous Python 2 la présence de __cmp__ (comparaison riche devant renvoyer -1/0/1 si le premier élément est inférieur, égal ou supérieur au second élément et peut se substituer à tous les autres opérateurs booléens, mais ayant disparu dans Python 3).

XV-6-g - Redéfinir les casts

Une classe peut redéfinir des opérations de cast qui lui seront appliquées. Ainsi, elle peut redéfinir __bool__ (booléen), __float__ (flottant), __int__ (entier), __bytes__ (chaîne), __str__ (chaîne) : permettent de surcharger le cast d’un objet en bool, float, int, bytes, str. À noter sous Python 2 que __bool__ se nomme alors __nonzero__ ainsi que la présence supplémentaire de __long__ et __unicode__ qui permettent de surcharger le cast d’un objet en long et en unicode. De plus, __bytes__ n’existe pas dans cette version.

Elle peut aussi redéfinir __repr__ (représentation) utilisé par repr().

XV-6-h - Rendre une classe « hashable »

Une classe peut devenir « hashable » ; c’est-à-dire pouvoir faire l’objet d’un calcul d’empreinte numérique ; par le biais de la méthode spécifique __hash__().

1. class maClass:

2. def __hash__(self):

3. return 123

5. >>> m=maClass()

6. >>> print(hash(m))

7. 123

XV-6-i - Gérer la liste des attributs listés

Il est possible de contrôler la liste des attributs donnés par la fonction dir() ; par le biais de la méthode spécifique __dir__(). Cette méthode, si elle est implémentée, doit impérativement renvoyer un itérable qui sera finalement transformé en liste.

1. class maClass:

2. def __dir__(self):

3. return "123"

5. >>> m=maClass()

6. >>> dir(m)

7. [1, 2, 3]

XV-6-j - Gérer le formatage

Il est possible de contrôler l’affichage d’une classe grâce à la méthode format() ou celle des « f-strings ».

class maClass:
def __init__(self, n):
self.n=n
def __format__(self, fmt):
print("fmt: [%s]" % fmt)
return "{0:{1}}".format(self.n, fmt)

>>> m=maClass(42)
>>> print("{:05d}".format(m))
fmt: [05d]
00042
>>> print(f"{m:04d}")
fmt: [04d]
0042

XV-7 - Les fonctions sont aussi des objets

Toute fonction étant elle aussi un objet, cela permet alors d'y créer des attributs personnels, et ce, même en dehors du corps de la fonction…

1. def toto(n):

2. toto.args.append(n)

3. print("Historique des arguments: %s" % toto.args)

5. >>> toto.args=[]

6. >>> toto(1)

7. Historique des arguments: [1]

8. >>> toto(2)

9. Historique des arguments: [1, 2]

10. >>> toto(3)

11. Historique des arguments: [1, 2, 3]

De plus, elle possède aussi des attributs et méthodes internes qui permettent au programmeur Python de les inspecter ou de les manipuler directement dans son code.

Voici une liste non exhaustive des éléments pouvant être utiles :

- fonction.__name__ : nom de la fonction ;

- fonction.__doc__ : documentation de la fonction (sera vu ultérieurement) ;

- fonction.__qualname__ : nom complet (dans le cas d'une fonction incluse dans un objet) de la fonction (n'existe pas dans Python 2) ;

- fonction.__module__ : nom du module de la fonction (sera vu ultérieurement) ;

- fonction.__defaults__ : tuple contenant les valeurs par défaut des arguments placés avant *args ;

- fonction.__kwdefaults__ : dictionnaire contenant les valeurs par défaut des arguments placés entre *args et **kwargs (n'existe pas dans Python 2 dans lequel *args est collé à **kwargs) ;

- fonction.__code__ : code de la fonction ;

- fonction.__code__.co_varnames : liste des paramètres de la fonction (y compris *args et **kwargs s'ils y sont) ;

- fonction.__code__.co_argcount : nombre de paramètres placés avant *args ;

- fonction.__code__.co_kwonlyargcount : nombre de paramètres placés entre *args et **kwargs (n'existe pas dans Python 2 dans lequel *args est collé à **kwargs).

XVI - Compléments sur les tuples, listes, ensembles, dictionnaires et chaînes

XVI-1 - Méthodes spécifiques applicables aux tuples et listes

Les types tuple et list disposent de méthodes spécifiques. Voici une liste non exhaustive des méthodes couramment utilisées :

- tuple.count(x)/ list.count(x) : renvoie le nombre d’éléments ayant la valeur x dans le tuple/liste ;

- tuple.index(x)/ list.index(x) : renvoie la position du premier élément du tuple/liste ayant la valeur x. Cette valeur doit être présente dans le tuple/liste sinon ça remonte une exception ValueError.

Le type list dispose de méthodes spécifiques supplémentaires dédiées à la modification. Voici une liste non exhaustive des méthodes couramment utilisées :

- list.append(x) : ajoute l'élément x à la fin de la liste ;

- list.clear() : vide totalement la liste (n'existe pas sous Python 2) ;

- list.copy() : renvoie une copie de la liste. Cette copie ne duplique réellement que les éléments de premier niveau de la liste (ne fonctionnera pas par exemple pour une liste de listes). N'existe pas sous Python 2 ;

- list.extend(itérable) : étend la liste en y ajoutant tous les éléments de l’itérable.

- list.insert(i, x) : insère l'élément x à la position i. Si la position dépasse le nombre d'éléments, l'insertion se fait alors à la fin de la liste. Ainsi, list.insert(0, x) insère l’élément en tête de la liste ; et list.insert(len(list), x) est équivalent à list.append(x) ;

- list.pop(i=‑1) : renvoie l’élément situé à la position i (par défaut la dernière de la liste) tout en le supprimant de la liste. Si la liste est vide, ça remonte une exception IndexError ;

- list.remove(x) : supprime de la liste le premier élément dont la valeur est x. Cette valeur doit être présente dans la liste sinon ça remonte une exception ValueError ;

- list.reverse() : inverse sur place l’ordre des éléments de la liste ;

- list.sort(key=None, reverse=False) : trie sur place les éléments de la liste. La façon de trier est paramétrée par les éléments key et reverse. À noter la présence sous Python 2 d'un argument supplémentaire cmp qui permet d'y placer une fonction de comparaison d'éléments. Cet argument a disparu dans Python 3, mais son comportement peut toutefois être reproduit au travers d'un objet transformant la fonction en clef et passé ensuite à l'argument key (sera vu ultérieurement).

XVI-2 - Méthodes spécifiques applicables aux ensembles

Le type set dispose de méthodes spécifiques. Voici une liste non exhaustive des méthodes couramment utilisées :

- set.add(x) : ajoute l'élément x à l’ensemble à condition qu’il n’y soit pas déjà (sinon pas d’effet) ;

- set.clear() : vide totalement l'ensemble ;

- set.copy() : renvoie une copie de l'ensemble. Cette copie ne duplique réellement que les éléments de premier niveau de l'ensemble (ne fonctionnera pas par exemple pour un ensemble d'ensembles) ;

- set.discard(x) : supprime de l’ensemble l’élément de valeur x si celui‑ci est présent (sinon pas d’effet) ;

- set.pop() : renvoie un élément pris au hasard dans l’ensemble tout en le supprimant de l’ensemble. Si l'ensemble est vide, ça remonte une exception KeyError ;

- set.remove(x) : supprime de l’ensemble l’élément de valeur x. Cette valeur doit être présente dans l'ensemble sinon ça remonte une exception KeyError ;

- set.update(itérable) : étend l’ensemble en y ajoutant tous les éléments de l’itérable qui n’y sont pas déjà.

XVI-3 - Méthodes spécifiques applicables aux dictionnaires

XVI-3-a - Pour les deux versions de Python

Le type dict dispose de méthodes spécifiques. Voici une liste non exhaustive des méthodes couramment utilisées :

- dict.keys() : renvoie toutes les clefs du dictionnaire ;

- dict.values() : renvoie toutes les valeurs du dictionnaire ;

- dict.items() : renvoie tous les tuples (clef, valeur) du dictionnaire ;

Ces trois méthodes renvoient une vue sous Python 3 (une vue est un objet dynamique qui s'adapte au dictionnaire, ainsi quand le dictionnaire change, la vue change de la même façon). Sous Python 2 elles renvoient une simple liste figée.

- dict.setdefault(k, default=None) : positionne la valeur donnée par défaut sur la clef k si celle-ci n’est pas présente, sinon ne modifie rien. Et renvoie ensuite la valeur associée à la clef k ;

- dict.update(key=valeur) : étend le dictionnaire en ajoutant (ou en les remplaçant si elles existent déjà) toutes les clefs demandées ;

- dict.update(itérable) : étend le dictionnaire en ajoutant (ou en les remplaçant s'ils existent déjà) tous les éléments de l’itérable. Chaque élément de l'itérable doit s'exprimer sous forme d'un tuple (clef, valeur) ;

- dict.update(dict) : étend le dictionnaire en ajoutant (ou en les remplaçant s'ils existent déjà) tous les éléments du dictionnaire ;

- dict.get(k, default=None) : récupère du dictionnaire l’élément situé à la clef k si cette clef existe et le renvoie ; sinon renvoie la valeur donnée par défaut ;

- dict.pop(k, default) : renvoie l’élément situé à la clef k si cette clef existe tout en le supprimant du dictionnaire. Si cette clef n’existe pas, mais qu’il y a une valeur par défaut, c’est cette valeur qui est renvoyée et dans ce cas le dictionnaire n’est pas modifié. Et si cette clef n'existe pas et qu'il n'y a pas de valeur par défaut, alors ça remonte une exception KeyError ;

- dict.popitem() : renvoie un tuple (clef+valeur) pris au hasard dans le dictionnaire tout en le supprimant du dictionnaire. Si le dictionnaire est vide alors ça remonte une exception KeyError ;

- dict.clear() : vide totalement le dictionnaire ;

- dict.copy() : renvoie une copie du dictionnaire. Cette copie ne duplique réellement que les éléments de premier niveau du dictionnaire (ne fonctionnera pas par exemple pour un dictionnaire de dictionnaires) ;

- dict.fromkeys(seq, value=None) : renvoie un dictionnaire dans lequel chaque élément de la séquence sera une clef du dictionnaire ayant la valeur value.

XVI-3-b - Spécificités Python 2

- dict.iterkeys() : retourne un générateur sur chaque clef du dictionnaire.

- dict.itervalues() : retourne un générateur sur chaque valeur du dictionnaire.

- dict.iteritems() : retourne un générateur sur chaque tuple (clef, valeur) du dictionnaire.

- dict.viewkeys() : retourne une vue des clefs du dictionnaire.

- dict.viewvalues() : retourne une vue des valeurs du dictionnaire.

- dict.viewitems() : retourne une vue des tuples (clef, valeur) du dictionnaire.

Comme expliqué précédemment, une vue est un objet dynamique qui s’adapte au dictionnaire. Si le dictionnaire change (rajoute ou supprime des clefs), la vue s’adapte au changement.

Un générateur lui, tel que vu au chapitre Les générateurs, renverra ce qui peut s’utiliser généralement comme une liste, mais ne s’adaptera pas aux changements du dictionnaire. C’est-à-dire que si le dictionnaire change entre la création du générateur et son utilisation, cela provoque une exception RuntimeError.

XVI-4 - Méthodes spécifiques applicables aux chaînes

Le type str dispose de méthodes spécifiques. Voici une liste non exhaustive des méthodes couramment utilisées :

- str.capitalize() : renvoie une copie de la chaîne avec son premier caractère mis en majuscule ;

- str.casefold() : renvoie une copie de la chaîne convertie en minuscules permettant une comparaison sans prendre en compte sa casse ;

- str.center(w, c=' ') : renvoie une copie de la chaîne centrée sur w positions remplies avec le caractère c ;

- str.count(x) : retourne le nombre d’éléments ayant la valeur x dans la chaîne ;

- str.encode(c) : renvoie une copie de la chaîne encodée dans le codec c ;

- str.endswith(x) : retourne un booléen indiquant si la chaîne se termine par x ;

- str.expandtabs(w) : renvoie une copie de la chaîne dans laquelle chaque tabulation est remplacée par w espaces ;

- str.find(x) : renvoie la position du x présent dans la chaîne (ou renvoie ‑1 s’il x n’y est pas) ;

- str.format(**d) : renvoie une copie de la chaîne en y incluant des variables ou expressions placées dans le dictionnaire d ;

- str.format_map(**d) : identique à str.format(**d) excepté que dans la précédente méthode, la variable d est copiée dans le format (création d’une nouvelle instance) tandis que dans ce cas elle est directement utilisée ;

- str.index(x) : identique à str.find(x) excepté que si le x n’y est pas, ça remonte une exception ValueError ;

- str.isalnum() : retourne un booléen indiquant si la chaîne ne contient que des caractères alphanumériques ;

- str.isalpha() : retourne un booléen indiquant si la chaîne ne contient que des caractères alphabétiques ;

- str.isascii() : retourne un booléen indiquant si la chaîne ne contient que des caractères ASCII ;

- str.isdecimal() : retourne un booléen indiquant si la chaîne ne contient que des caractères permettant de former des nombres décimaux.

- str.isdigit() : retourne un booléen indiquant si la chaîne ne contient que des caractères permettant de créer des nombres en plus des nombres décimaux (ex nombres de Kharosthi) ;

- str.isidentifier() : retourne un booléen indiquant si la chaîne est un identificateur ou un mot‑clef Python ;

- str.islower() : retourne un booléen indiquant si la chaîne ne contient que des caractères minuscules ;

- str.isnumeric() : retourne un booléen indiquant si la chaîne ne contient que des caractères numériques ;

- str.isprintable() : retourne un booléen indiquant si la chaîne ne contient que des caractères affichables sur un écran ;

- str.isspace() : retourne un booléen indiquant si la chaîne ne contient que des caractères de type « espace » (espace, tabulation, etc.) ;

- str.istitle() : retourne un booléen indiquant si la chaîne est assimilable à un titre (premier caractère de chaque mot en majuscule) ;

- str.isupper() : retourne un booléen indiquant si la chaîne ne contient que des caractères majuscules ;

- str.join(iter) : renvoie une nouvelle chaîne contenant la chaîne appelante placée entre chaque élément de l’itérable iter ;

- str.ljust(w, c=' ') : renvoie une copie de la chaîne justifiée à gauche sur w positions remplies avec le caractère c ;

- str.lower() : renvoie une copie de la chaîne convertie en minuscules ;

- str.lstrip() : renvoie une copie de la chaîne dont les espaces et autres caractères inutiles de gauche ont été supprimés ;

- str.maketrans(x) : renvoie une table de translation (un dictionnaire) basée sur x qui servira ensuite à str.translate() ;

- str.partition(c) : renvoie un tuple de 3 éléments contenant la partie de chaîne placée avant le premier c, l’élément c et la partie de chaîne placée après ce c ;

- str.replace(x, y) : renvoie une copie de la chaîne dans laquelle tout caractère x a été remplacé par y ;

- str.rfind(x) : identique à str.find() sauf que la recherche se fait à partir de la fin et en remontant vers le début ;

- str.rindex(x) : identique à str.index() sauf que la recherche se fait à partir de la fin et en remontant vers le début ;

- str.rjust(w, c=' ') : renvoie une copie de la chaîne justifiée à droite sur w positions remplies avec le caractère c ;

- str.rpartition(x) : identique à str.partition() sauf que la recherche du c se fait à partir de la fin et en remontant vers le début ;

- str.rstrip() : renvoie une copie de la chaîne dont les espaces et autres caractères inutiles de droite ont été supprimés ;

- str.split(c=' ') : renvoie une liste contenant la chaîne découpée sur chaque élément c ;

- str.splitlines(keepends=False) : renvoie une liste contenant la chaîne découpée sur chaque fin de ligne (\n), la fin de ligne sera incluse si le paramètre keepends est mis à True ;

- str.startswith(x) : retourne un booléen indiquant si la chaîne commence par x ;

- str.strip() : combine les effets de str.lstrip() et str.rstrip() ;

- str.swapcase() : renvoie une copie de la chaîne dans laquelle chaque minuscule est devenue une majuscule et inversement ;

- str.title() : renvoie une copie de la chaîne dans laquelle chaque mot a son premier caractère mis en majuscule ;

- str.translate(d) : renvoie une copie de la chaîne dans laquelle chaque clef du dictionnaire d a été remplacée par sa valeur associée. Le dictionnaire peut être créé manuellement ou bien provenir de str.maketrans() ;

- str.upper() : renvoie une copie de la chaîne convertie en majuscules.

XVI-5 - Fonctions externes

Python possède aussi des fonctions spécifiques dédiées à la manipulation de chaînes, tuples, listes, ensembles et dictionnaires (pour ces derniers, les manipulations s’appliquent aux clefs) :

- all(iterable) : retourne True si tous les éléments de iterable sont considérés comme « vrai ». Cette instruction fonctionne à l’économie (elle s’arrête dès qu’une certitude est acquise donc au premier élément qui n’est plus « vrai ») ;

- any(iterable) : retourne True si un des éléments de iterable est considéré comme « vrai ». Cette instruction fonctionne à l’économie (elle s’arrête dès qu’une certitude est acquise donc au premier élément qui est « vrai ») ;

- filter(fonction, iterable) : renvoie une nouvelle liste contenant seulement les éléments de iterable pour lesquels la fonction fonction renvoie True ou toute expression considérée comme « vraie ». La fonction fonction doit être prévue pour n’accepter qu’un seul argument, mais peut être mise à None. Dans ce dernier cas, la nouvelle liste contiendra alors les éléments de l’itérable considérés nativement comme True ;

- map(fonction, iterable, …) : retourne un générateur contenant chaque élément de iterable après l’avoir fait traiter par la fonction fonction. Il peut traiter plusieurs itérables en parallèle ; la fonction fonction devant alors être prévue pour accepter autant de paramètres qu’il y a d’itérables demandés. Les éléments des itérables sont ensuite récupérés parallèlement les uns avec les autres (ou None pour les itérables n’ayant pas assez d’éléments par rapport aux autres) ;

- min(iterable, default=objet, key=None) / max(iterable, default=objet, key=None) : renvoie le plus petit/plus grand élément de iterable selon l’action de la clef demandée dans key (action qui doit être une fonction n’acceptant qu’un paramètre présumé être un élément de la liste), fonction associée à chaque élément de l’itérable si ce paramètre est demandé ou selon la valeur numérique de chaque élément sinon. Si l’itérable est vide, retourne alors l’objet mis dans default ou une exception ValueError si ce paramètre n’a pas été invoqué ;

- reduce(fonction, iterable, initializer=None) : applique la fonction fonction à chacun des éléments de iterable pris comme second argument en utilisant le résultat de l’élément précédent (ou par défaut initializer pour le premier élément qui, lui, ne bénéficie pas de résultat précédent) comme premier argument. Renvoie le résultat du traitement du dernier élément de l’itérable. La fonction fonction doit être prévue pour n’accepter que deux arguments. Cette méthode était naturellement présente dans Python 2. Elle doit être maintenant importée du module functools pour être disponible dans Python 3 ;

- sorted(iterable, key=None, reverse=False) : renvoie une liste croissante ou décroissante selon la valeur de reverse et contenant iterable trié selon l’action de la clef demandée dans key (action qui doit être une fonction n’acceptant qu’un paramètre présumé être un élément de la liste), fonction associée à chaque élément de l’itérable si ce paramètre est demandé ou selon la valeur numérique de chaque élément sinon ;

- sum(iterable, start=0) : retourne la somme des éléments de iterable (ces éléments étant tous censés pouvoir s’additionner) en initialisant cette somme à la valeur start ;

- zip(iterable, …) : retourne un objet zip (objet assimilable à une liste) contenant tous les éléments de chaque iterable récupérés en parallèle et accolés. Si les divers itérables n’ont pas le même nombre d’éléments, l’ensemble s’adapte à l’itérable le plus petit.

XVI-6 - Modifier une séquence en cours d’itération

Quand on itère sur une séquence modifiable (liste, ensemble), il est très dangereux de modifier ladite séquence dans le corps de la boucle qui l’itère parce que Python réévalue la séquence à chaque itération. Exemple :

1. >>> tab=[1,]

2. >>> for x in tab:

3. ...    print(x)

4. ...    tab.append(x+1)

5. ...

6. 1

7. 2

8. 3

9. …(à l’infini)…

Dans le cas d’un ajout, si l’utilisateur a besoin d’ajouter des éléments sur la séquence durant l’exécution de la boucle qui l'itère, il doit alors utiliser une copie de la séquence lors de son évaluation par la boucle. Différentes syntaxes peuvent être utilisées comme une copie à chaud…

1. >>tab=[1, 2, 3, 4, 5]

2. >>> for x in tuple(tab):            # La boucle itèrera sur un tuple mémoire créé depuis tab

3. ...    print(x)

4. ...    tab.append(x+1)

5. ...

6. 1

7. 2

8. 3

9. 4

10. 5

11. >>> print(tab)

12. [1, 2, 3, 4, 5, 2, 3, 4, 5, 6]

… ou un slice qui lui aussi renvoie une copie de l’élément originel.

1. >>tab=[1, 2, 3, 4, 5]

2. >>> for x in tab[:]:                # La boucle itèrera sur un slice mémoire créé depuis tab

3. ...    print(x)

4. ...    tab.append(x+1)

5. ...

6. 1

7. 2

8. 3

9. 4

10. 5

11. >>> print(tab)

12. [1, 2, 3, 4, 5, 2, 3, 4, 5, 6]

Dans le cas d’une suppression, c’est encore plus subtil, car la suppression de l’élément i décale alors immédiatement la position de tous les éléments qui suivent (la suppression de l’élément [3] décale alors l’élément [4] qui devient [3] etc). Et la combinaison de l’itération qui passe à l’élément suivant à chaque tour pendant que l’élément suivant est décalé vers le précédent à chaque suppression provoque alors la perte d’un élément sur deux.

Exemple de ce qui se passe si on tente de supprimer naïvement les éléments pairs d’un tableau contenant les 10 premiers entiers…

1. >>> tab=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

2. >>> for i in range(len(tab)):

3. ...    print("i=%d, tab=%d" % (i, tab[i]))             # Pour montrer l’élément en cours

4. ...    if (tab[i] % 2) == 0: del tab[i]                # Ça semble correspondre à la demande

5. ...

6. i=0, tab=1

7. i=1, tab=2

8. i=2, tab=4

9. i=3, tab=6

10. i=4, tab=8

11. i=5, tab=10

12. Traceback (most recent call last):

13. File "<stdin>", line 2, in <module>

14. IndexError: list index out of range

15. >>>

Comme on le voit, la suppression de l’élément pair décale vers la gauche l’élément impair qui le suit (et tout le reste du tableau par ricochet). Mais comme l’indice se décale lui vers la droite en même temps, non seulement on ne traite pas les éléments impairs (sinon on les verrait lors du print()), mais en plus l’indice finit par dépasser la taille du tableau qui décroit et par provoquer une exception.

Une première solution pourra être basée sur un indice qui évolue ou pas selon le cas…

1. >>> tab=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

2. >>> i=0

3. >>> while i<len(tab):

4. ...    if (tab[i]%2) == 0: del tab[i]

5. ...    else: i+=1

6. ...

7. >>> print(tab)

8. [1, 3, 5, 7, 9]

Une seconde solution plus simple sera de traiter la liste à l’envers (l’indice remontant vers 0 plus vite que ce que le tableau diminue)…

1. >>> tab=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

2. >>> for i in range(len(tab)-1, -1, -1):

3. ...    if (tab[i]%2) == 0: del tab[i]

4. ...

5. >>> print(tab)

6. [1, 3, 5, 7, 9]

Sinon, on pourra se tourner en troisième recours vers une création à chaud de la liste attendue à partir d’une liste en intension basée sur la liste initiale…

1. >>> tab=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

2. >>> tab=[x for x in tab if (x%2) != 0]

3. >>> print(tab)

4. [1, 3, 5, 7, 9]

XVII - Les closures

XVII-1 - Introduction

Il a été vu dans le chapitre sur les fonctions que celles‑ci peuvent en inclure d’autres…

1. >>> def fct():

2. ...    def xxx():

3. ...        print("xxx")

4. ...    xxx()

5. ...    print("ok")

7. >>> fct()

8. xxx

9. ok

Si maintenant, au lieu d’afficher une chaîne en dur, la fonction incluse affichait une variable provenant de la fonction supérieure…

1. >>> def fct():

2. ...    v=123

3. ...    def xxx():

4. ...        print(v)

5. ...    xxx()

6. ...    print("ok")

8. >>> fct()

9. 123

10. ok

Cela fonctionne encore, car la fonction interne a accès à la variable de la fonction supérieure. Cela se fait parce que dans la fonction du plus haut niveau, Python crée un espace mémoire spécial pour stocker la référence à cette variable « v ».

Cet espace mémoire est appelé « closure ».

XVII-2 - L’instruction « nonlocal »

Nous venons de voir que la fonction interne avait accès à la variable v située dans l’espace mémoire « closure ». Mais que se passe-t-il si la fonction interne veut modifier ladite variable v… ?

1. >>> def fct():

2. ...    v=123

3. ...    def xxx():

4. ...        v+=1

5. ...        print(v)

6. ...    xxx()

7. ...    print("ok")

8. ...

9. >>> fct()

10. Traceback (most recent call last):

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

12. File "<stdin>", line 6, in fct

13. xxx()

14. File "<stdin>", line 2, in xxx

15. v+=1

16. UnboundLocalError: local variable 'v' referenced before assignment

On se retrouve avec un souci analogue à celui déjà vu lors de la tentative de modification d’une variable globale par une fonction (cf. (chapitre sur la visibilité des variables)).

Toutefois, l’instruction global ne convient pas, car la variable v n’est pas située dans le scope global.

Pour accéder à la variable v, Python 3 a introduit une instruction spécifique (qui n’existe pas dans Python 2) qui est nonlocal.

Cette instruction indique que la variable n’est pas locale. Et n’étant pas globale non plus, elle fait alors partie de l’espace mémoire « closure » qui est simplement l’espace mémoire d’une fonction de premier niveau.

1. >>> def fct():

2. ...    v=123

3. ...    def xxx():

4. ...        nonlocal v

5. ...        v+=1

6. ...        print(v)

7. ...    xxx()

8. ...    print("ok")

9. ...

10. >>> fct()

11. 124

12. ok

Note : pour obtenir le même résultat avec Python 2, il est alors nécessaire de passer par l’objet fonction en lui mettant la variable en tant qu’attribut (cf. (chapitre sur les fonctions qui sont aussi des objets)).

1. >>> # Exemple Python 2 (qui fonctionne aussi sous Python 3)

2. >>> def fct():

3. ...    def xxx():

4. ...        fct.v+=1

5. ...        print(fct.v)

6. ...    fct.v=123

7. ...    xxx()

8. ...    print("ok")

9. ...

10. >>> fct()

11. 124

12. ok

XVII-3 - À quoi cela sert-il ?

Cela permet d’attacher un état à la fonction.

Imaginons l’étal d’un marchand. Celui-ci va devoir établir et afficher le prix de ses fruits.

Une première solution pourra être de passer par des classes et créer un objet…

1. >>> class etal:

2. ...    def __init__(self, fruit):

3. ...        self.__fruit=fruit

4. ...        self.__prix=0

5. ...    def maj(self, modif):

6. ...        self.__prix+=modif

7. ...        print("Le prix des %s est de %d€" % (self.__fruit, self.__prix))

8. ...

9. >>> orange=etal("oranges")

10. >>> pomme=etal("pommes")

11. >>> orange.maj(5)

12. Le prix des oranges est de 5€

13. >>> pomme.maj(2)

14. Le prix des pommes est de 2€

15. >>> orange.maj(-1)

16. Le prix des oranges est de 4€

17. >>> pomme.maj(4)

18. Le prix des pommes est de 6€

Cela fonctionne, mais reste une solution lourde pour ce besoin si simple. Les closures permettent d’obtenir le même résultat, mais en passant simplement par des fonctions au lieu de classes.

1. >>> def etal(fruit):

2. ...    def maj(modif):

3. ...        nonlocal prix

4. ...        prix+=modif

5. ...        print("Le prix des %s est de %d€" % (fruit, prix))

6. ...    prix=0

7. ...    return maj

9. >>> orange=etal("oranges")

10. >>> pomme=etal("pommes")

11. >>> orange(5)

12. Le prix des oranges est de 5€

13. >>> pomme(2)

14. Le prix des pommes est de 2€

15. >>> orange(-1)

16. Le prix des oranges est de 4€

17. >>> pomme(4)

18. Le prix des pommes est de 6€

Notez bien que la fonction etal(), quand elle est exécutée, retourne l’objet de la fonction interne maj, (pour simplifier, elle retourne juste son code et non le résultat de son exécution). Ainsi, tout appel à la variable orange() utilisée à ce moment-là comme fonction appelée fait en réalité appel au code de la fonction maj(). Mais chaque variable orange ou pomme étant différente, chacune d’elles possède sa propre closure incluant sa propre variable prix.

Et pour faire la même chose sous Python 2, on peut alors revenir vers les attributs de l’objet fonction (méthode qui reste bien évidemment elle aussi utilisable sous Python 3)…

1. >>> # Exemple Python 2 (qui fonctionne aussi sous Python 3)

2. >>> def etal(fruit):

3. ...    def maj(modif):

4. ...        etal.prix+=modif

5. ...        print("Le prix des %s est de %d€" % (fruit, etal.prix))

6. ...    etal.prix=0

7. ...    return maj

8. ...

9. >>> orange=etal("oranges")

10. >>> pomme=etal("pommes")

11. >>> orange(5)

12. Le prix des oranges est de 5€

13. >>> pomme(2)

14. Le prix des pommes est de 2€

15. >>> orange(-1)

16. Le prix des oranges est de 4€

17. >>> pomme(4)

18. Le prix des pommes est de 6€

XVIII - Les métaclasses

Les métaclasses sont une notion assez complexe qui fait appel à un très haut degré d’abstraction. Et pour 99% des utilisateurs, ne seront jamais utiles. Mais elles existent donc…

Pour résumer brièvement, si une classe sert à créer un objet, alors une métaclasse sert à créer une classe.

Ainsi si UnObjet=UneClasse() alors UneClasse=UneMetaClass().

XVIII-1 - La fonction « type() »

On sait que tout objet est instance d’une classe. On dit aussi que la classe est le type de l’objet. Et donc, tout objet a forcément un type.

Le type d’un objet peut être récupéré grâce à la fonction type().

1. >>> type(5)

2. <class 'int'>

3. >>> type("Hello")

4. <class 'str'>

5. >>> type(lambda: None)

6. <class 'function'>

Mais si les classes sont des objets, quel est alors leur type ?

1. >>> type(object)

2. <class 'type'>

Ainsi, le type d’un objet est un… type. On ne peut pas faire plus simple comme définition !

Or, on a vu depuis le début que tout type Python possède une fonction dédiée permettant de le créer (il existe un type int ainsi qu’une fonction int() permettant de créer un int ; il existe un type float ainsi qu’une fonction float() permettant de créer un float ; il existe un type tuple ainsi qu’une fonction tuple() permettant de créer un tuple ; etc.). Donc, de même puisqu’il existe un type type, il existe aussi une fonction type() permettant de créer un type c’est-à-dire un objet.

Pour créer un objet par cette fonction, on doit lui passer le nom de la classe à créer (évident), un tuple contenant le ou les objets dont il veut hériter s’il doit hériter (logique) et un dictionnaire analogue à l’attribut __dict__ c’est-à-dire contenant les attributs que l’on veut avoir dans l’objet et leurs valeurs.

Exemple :

1. >>> v=type("vehicule", (), {"vMax" : 60})

2. >>> print(v)

3. <class '__main__.vehicule'>

4. >>> print(v.vMax)

5. 60

6. >>> vv=type("voiture", (v,), {"marque" : "lada"})

7. >>> print(vv)

8. <class '__main__.voiture'>

9. >>> print(vv.vMax)

10. 60

11. >>> print(vv.marque)

12. 'lada'

En fait, ceci est le comportement naturel de la fonction type() qui a pour but officiel de créer un objet. Son second comportement (retourner le type de l’objet qu’on lui passe en paramètre) qu’on a vu depuis le début n’est qu’un comportement particulier.

Et puisque la fonction type() permet de créer un objet (en particulier aussi une classe), alors c’est le premier exemple d’une métaclasse. En fait, c’est la métaclasse de base dont Python se sert pour créer toutes les classes quand il traite l’instruction class.

Le même exemple écrit sous forme classique :

1. >>> class vehicule:

2. ...    def __init__(self, vMax):

3. ...        self.vMax=vMax

4. ...

5. >>> v=vehicule(60)

6. >>> print(v)

7. <class '__main__.vehicule'>

8. >>> print(v.vMax)

9. 60

10. >>> class voiture(vehicule):

11. ...    def __init__(self,  marque, vMax):

12. ...        super().__init__(vMax)

13. ...        self.vmarque=marque

14. ...

15. >>> vv=voiture("Lada", 60)

16. >>> print(vv)

17. <class '__main__.voiture'>

18. >>> print(vv.vMax)

19. 60

20. >>> print(vv.marque)

21. 'lada'

XVIII-2 - Créer sa propre fonction génératrice

Puisque les fonctions peuvent retourner des objets, elles peuvent aussi retourner des classes.

1. def makeClass(nom):

2. if nom == "carre":

3. class carre:

4. def __init__(self, cote): self.cote=cote

5. def surface(self): return self.cote ** 2

6. return carre                     # Attention pas de parenthèses, on retourne un type !!!

7. elif nom == "cercle":

8. class cercle:

9. def __init__(self, rayon):

10. self.rayon=rayon

11. def surface(self): return self.rayon ** 2 * 3.14

12. return cercle                    # Attention pas de parenthèses, on retourne un type !!!

13. else:

14. return None

16. >>> cFigure=makeClass("cercle")          # cFigure est une classe "cercle"

17. >>> cFigure, type(cFigure)

18. <class '__main__.makeClass.<locals>.cercle'> <class 'type'>

19. >>> figure=cFigure(5)                    # Je peux alors instancier un objet cercle

20. >>> figure

21. <__main__.makeClass.<locals>.cercle object at 0x7fea924297f0>

22. >>> figure.surface()

23. 78.5

24. >>>

25. >>> cFigure=makeClass("carre")           # cFigure est une classe "carre"

26. >>> cFigure, type(cFigure)

27. <class '__main__.makeClass.<locals>.carre'> <class 'type'>

28. >>> figure=cFigure(5)                    # Je peux alors instancier un objet carre

29. >>> figure

30. <__main__.makeClass.<locals>.carre object at 0x7fea92429860>

31. >>> figure.surface()

32. 25

XVIII-3 - Puis passer par une classe

Et puisqu’une classe peut aussi faire des trucs au travers de ses méthodes (qui sont des fonctions), alors elle peut aussi créer une classe. Ne reste qu’à découvrir sa syntaxe.

Tout d’abord, qu’est-ce qu’une classe dans Python ? C’est juste un enrobage d’un simple dictionnaire : le __dict__(). C’est lui qui contient la liste des attributs et méthodes de la classe. Ainsi, créer une classe, c’est simplement remplir (ou juste modifier) ces attributs (ce __dict__()).

Nous allons donc créer une fonction qui recevra une classe en paramètre et qui modifiera les attributs en leur rajoutant (pour l’exemple) une chaîne correspondant au nom de la classe. Ainsi, pour la classe toto, l’attribut xxx deviendra toto_xxx (oui, pas vraiment utile, mais c’est juste pour l’exemple !).

Cette fonction ayant le même comportement que la fonction type(), elle doit avoir une signature identique, c’est-à-dire recevoir le nom de la classe, les ancêtres et les attributs.

Pour l’exemple, cette fonction qui servira à personnaliser notre classe s’appellera custom().

1. def custom(cls, parents, attr):

2. # Un petit affichage préalable pour montrer ce qui se passe…

3. print("custom: cls=%s, parents=%s, attr=%s" % (cls, parents, attr))

5. # Elle commence par définir la chaîne à rajouter en fonction du nom de la classe

6. keyword={

7. "woody" : "cowboy",

8. "buzz" : "eclair",

9. }.get(cls)

11. # Ensuite elle crée le dictionnaire des nouveaux attributs

12. nouveaux_attr=dict()

14. # Puis elle traite les attributs de la classe reçue en paramètre

15. for (k, v) in attr.items():

16. # S’il y a un mot clef (classe particulière) et que l’attribut n’est pas spécial

17. # Le mot à rajouter est créé sinon il reste vide

18. word="%s_" % keyword if keyword is not None and not k.startswith("__") else ""

20. # L’attribut est copié dans le dictionnaire avec son nouvel identificateur

21. nouveaux_attr[word + k]=v

23. # La fonction retourne une nouvelle classe créée dynamiquement depuis la classe reçue

24. return type(cls, parents, nouveaux_attr)

Maintenant, nous écrivons nos classes en leur indiquant qu’elles sont métaclassées par la fonction custom. Cela se fait en écrivant explicitement metaclass=nom_de_la_fonction entre parenthèses après le nom de la classe.

Attention, le message qui est indiqué après chaque classe dans cet exemple n’est pas à écrire, il provient du print() situé au début de la fonction custom()…

1. >>> class woody(metaclass=custom):

2. ...    def action(self):

3. ...        print("Il y a un serpent dans ma botte !!!")

4. ...    def dodo(self):

5. ...        print("wwwwww")

6. ...

7. custom: cls=woody, parents=(), attr={'action': <function action at 0x7fc0e75b7758>, 'dodo': <function dodo at 0x7fc0e75b77d0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}

8. >>>

9. >>> class buzz(metaclass=custom):

10. ...    def action(self):

11. ...        print("Vers l'infini et au delà !!!")

12. ...    def dodo(self):

13. ...        print("zzzzzz")

14. ...

15. custom: cls=buzz, parents=(), attr={'action': <function action at 0x7fc0e75b7848>, 'dodo': <function dodo at 0x7fc0e75b78c0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}

À noter : sous Python 2, la métaclasse se crée par le biais d’un attribut statique particulier __metaclass__. Là aussi apparaîtra évidemment le même message.

1. # Spécifique Python 2

2. >>> class woody:

3. ...    __metaclass__=custom

4. ...    def action(self):

5. ...        print("Il y a un serpent dans ma botte !!!")

6. ...    def dodo(self):

7. ...        print("wwwwww")

8. ...

9. custom: cls=woody, parents=(), attr={'action': <function action at 0x7fc0e75b7758>, 'dodo': <function dodo at 0x7fc0e75b77d0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}

10. >>>

11. >>> class buzz:

12. ...    __metaclass__=custom

13. ...    def action(self):

14. ...        print("Vers l'infini et au delà !!!")

15. ...    def dodo(self):

16. ...        print("zzzzzz")

17. ...

18. custom: cls=buzz, parents=(), attr={'action': <function action at 0x7fc0e75b7848>, 'dodo': <function dodo at 0x7fc0e75b78c0>, '__module__': 'meta', '__metaclass__': <function custom at 0x7fc0e75b7668>}

Ensuite, il ne reste qu’à instancier nos classes et les tester.

1. >>> w=woody()

2. >>> dir(w)

3. ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'cowboy_action', 'cowboy_dodo']

5. >>> b=buzz()

6. >>> dir(b)

7. ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'eclair_action', 'eclair_dodo']

Chaque méthode action() et dodo() de chaque objet a bien été renommée en fonction du nom de l’objet lui‑même. On peut donc les utiliser par leur nouveau nom.

1. >>> w.cowboy_action()

2. Il y a un serpent dans ma botte !!!

3. >>> w.cowboy_dodo()

4. wwwwww

5. >>> b.eclair_action()

6. Vers l’infini et au-delà !

7. >>> b.eclair_dodo()

8. zzzzzz

Et bien évidemment, les méthodes originelles action() et dodo() définies par le programmeur des classes woody et buzz n’existent plus puisque la fonction custom() a changé leurs identificateurs.

1. >>> w=woody()

2. >>> w.action()

3. Traceback (most recent call last):

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

5. AttributeError: 'woody' object has no attribute 'action'

6. >>> w.dodo()

7. Traceback (most recent call last):

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

9. AttributeError: 'woody' object has no attribute 'dodo'

10. >>>

11. >>> b=buzz()

12. >>> b.action()

13. Traceback (most recent call last):

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

15. AttributeError: 'buzz' object has no attribute 'action'

16. >>> b.dodo()

17. Traceback (most recent call last):

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

19. AttributeError: 'buzz' object has no attribute 'dodo'

XVIII-4 - Code complet

Voici le code complet directement exécutable de ce chapitre.

1. #!/usr/bin/env python3

2. # coding: utf-8

4. # La fonction de customisation

5. def custom(cls, parents, attr):

6. # Un petit affichage préalable pour montrer ce qui se passe…

7. print("custom: cls=%s, parents=%s, attr=%s" % (cls, parents, attr))

9. # Elle commence par définir la chaîne à rajouter en fonction du nom de la classe

10. keyword={

11. "woody" : "cowboy",

12. "buzz" : "eclair",

13. }.get(cls)

15. # Ensuite elle crée le dictionnaire des nouveaux attributs

16. nouveaux_attr=dict()

18. # Puis elle traite les attributs de la classe reçue en paramètre

19. for (k, v) in attr.items():

20. # S’il y a un mot clef (classe particulière) et que l’attribut n’est pas spécial

21. # Le mot à rajouter est créé sinon il reste vide

22. word="%s_" % keyword if keyword is not None and not k.startswith("__") else ""

24. # L’attribut est copié dans le dictionnaire avec son nouvel identificateur

25. nouveaux_attr[word + k]=v

26. # for

28. # La fonction retourne une nouvelle classe créée dynamiquement depuis la classe reçue

29. return type(cls, parents, nouveaux_attr)

30. # custom()

32. # L’objet "woody" qui sera customisé par la fonction

33. class woody(metaclass=custom):

34. def action(self):

35. print("Il y a un serpent dans ma botte !")

36. def dodo(self):

37. print("wwwwww")

38. # class woody

40. # L’objet "buzz" qui sera customisé par la fonction

41. class buzz(metaclass=custom):

42. def action(self):

43. print("Vers l'infini et au-delà !")

44. def dodo(self):

45. print("zzzzzz")

46. # class buzz

48. # Test de l’objet "woody"

49. w=woody()

50. w.cowboy_action()

51. w.cowboy_dodo()

53. # Test de l’objet "buzz"

54. b=buzz()

55. b.eclair_action()

56. b.eclair_dodo()

XVIII-5 - Pour finir

Si vous vous demandez si vous avez besoin des métaclasses, alors ce n’est pas le cas. Les gens qui en ont vraiment besoin le savent avec certitude et n’ont pas besoin d’explications sur la raison (Tim Peters).

XIX - Les erreurs et les exceptions

XIX-1 - Préambule

Depuis l'avènement des langages de haut niveau, on fait maintenant la distinction entre « erreurs » et « exceptions ».

Une erreur concerne principalement la syntaxe. Elle se produit une seule fois lors de l’analyse syntaxique du programme et, une fois corrigée, disparaît à jamais. Elle peut aussi concerner la logique du code par rapport au résultat attendu, mais dans ce cas, elle ne se révèle pas à l’exécution.

Une exception concerne un événement non attendu ou un élément incorrect survenant durant l'exécution du programme et provoquant un comportement erratique à ce moment‑là. Sa caractéristique principale est son côté fortuit, voire aléatoire, lié au fait qu’on ne peut jamais vraiment tout contrôler surtout quand les données proviennent d’un flux externe au programme.

1. >>> 1/0

2. Traceback (most recent call last):

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

4. ZeroDivisionError: integer division or modulo by zero

Une exception, une fois levée, remonte toute la chaîne des instructions qui l'ont amenée pour arriver finalement à l'OS qui exécute le programme.

1. >>> def a(): 1/0

2. ...

3. >>> def b(): a()

4. ...

5. >>> b()

6. Traceback (most recent call last):

7. File "<stdin>", line 3, in <module>

8. File "<stdin>", line 2, in b

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

10. ZeroDivisionError: integer division or modulo by zero

Cet historique se lit de bas en haut. Donc, dans cet exemple, il y a une exception ZeroDivisionError qui se produit à la ligne 1 dans la fonction a. Cette fonction a est appelée à la ligne 2 dans la fonction b. Et cette fonction b est appelée à la ligne 3 du module (programme principal).

L'affichage de l'historique des appels est important, car la cause d'une exception ne se produit pas forcément là où l'exception est levée. Ainsi, dans l'exemple précédent, la cause est effectivement là où l'exception se produit, mais dans le code suivant…

1. >>> def a(n): 1/n

2. ...

3. >>> def b(): a(0)

4. ...

5. >>> b()

6. Traceback (most recent call last):

7. File "<stdin>", line 3, in <module>

8. File "<stdin>", line 2, in b

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

10. ZeroDivisionError: integer division or modulo by zero

… l'exception et les messages sont exactement les mêmes, pourtant, si on analyse le code, on se rend compte que la cause de l'exception n'est pas dans la fonction a qui se contente d'utiliser ce qu'on lui passe pour travailler, mais dans la fonction b qui appelle a avec un mauvais paramètre.

XIX-2 - Se prémunir

La première façon de se prémunir contre les exceptions consiste généralement à les détecter avant qu'elles ne se produisent. Cependant, il existe des situations où cette détection est trop compliquée, voire impossible.

1. int(input("Entrez un nombre : "))

2. # Essayez de rentrer une chaîne à ce moment-là…

La seconde façon consiste alors à englober le code « sensible » dans un bloc try…except. Les instructions du bloc seront exécutées, mais si une exception se produit durant cette exécution ; et si cette exception a été prévue dans un cas except, alors les instructions situées dans ce cas except seront exécutées, mais l’exception sera neutralisée et le programme continuera son travail.

1. try:

2. x=input("Entrez un nombre : ")

3. int(x)

4. except ValueError as e:

5. print("Désolé, %s n'est pas un nombre – Exception %s" % (x, e))

Il est possible de gérer plusieurs exceptions de façon distincte…

1. def calcul(n): return n ** 0.5/n

3. try:

4. x=input("Entrez un nombre : ")

5. print(calcul(int(x)))

6. except ValueError as e:

7. print("Désolé, %s n’est pas un nombre – Exception %s" % (x, e))

8. except ZeroDivisionError as e:

9. print("Désolé, on ne divise pas par zéro – Exception %s" % e)

… ou de gérer d'un coup plusieurs exceptions en les regroupant dans un tuple.

1. def calcul(n): return n ** 0.5/n

3. try:

4. x=input("Entrez un nombre : ")

5. print(calcul(x))

6. except (ValueError, ZeroDivisionError) as e:

7. print("Désolé, ce calcul est impossible sur %s– Exception %s" % (x, e))

XIX-3 - Les instructions « else » et « finally »

Le bloc try contient deux autres instructions possibles.

L'instruction else va s'exécuter si aucune exception n'a été levée.

1. def calcul(n): return n ** 0.5/n

3. try:

4. x=input("Entrez un nombre : ")

5. res=calcul(int(x))

6. except (ValueError, ZeroDivisionError) as e:

7. print("Désolé, ce calcul est impossible sur %s– Exception %s" % (x, e))

8. else:

9. print("Le résultat du calcul de %d est %d" % (x, res))

C'est relativement équivalent à ceci :

1. def calcul(n): return n ** 0.5/n

3. try:

4. x=input("Entrez un nombre : ")

5. res=calcul(int(x))

6. print("Le résultat du calcul de %d est %d" % (x, res))

7. except (ValueError, ZeroDivisionError) as e:

8. print("Désolé, ce calcul est impossible sur %s– Exception %s" % (x, e))

Toutefois, rajouter un bloc else permet de faire la différence entre ce qui est vraiment sensible et ce qui ne l’est pas ; tout en gardant l’ensemble à l’esprit. Ainsi, dans cet exemple, les affichages étant secondaires par rapport aux tentatives de calcul qui, elles, sont le cœur de l’opération, ils ont fortement intérêt à être déportés dans un bloc else.

L'instruction finally permet de rajouter un bloc d'instructions qui s'exécutera dans tous les cas.

1. def calcul(n): return n ** 0.5/n

3. try:

4. x=input("Entrez un nombre : ")

5. res=calcul(int(x))

6. except (ValueError, ZeroDivisionError) as e:

7. print("Désolé, ce calcul est impossible sur %s– Exception %s" % (x, e))

8. else:

9. print("Le résultat du calcul de %d est %d" % (x, res))

10. finally:

11. print("Quoi qu'il arrive, ceci sera exécuté")

Cela permet généralement de libérer des ressources éventuellement allouées avant ou dans le try.

À noter que le bloc finally s’exécute même si on quitte le try avant de l’atteindre.

1. def toto():

2. try:

3. return "Hello"

4. finally:

5. print("fin")

7. >>> toto()

8. fin

9. 'Hello'

XIX-4 - L’instruction « raise »

L'instruction raise permet à l’utilisateur de lever lui‑même l’exception de son choix. Mais il ne peut lever qu’une exception faisant partie de l’arbre des exceptions Python (qui commence à BaseException), quitte à lui ajouter éventuellement ensuite un message personnalisé. Dans ce dernier cas, le message doit être placé entre parenthèses (Python 2 autorise aussi la virgule).

1. def calcul(n):

2. if not isinstance (n, (int, float)):

3. raise ValueError("Ce calcul ne peut se faire qu’avec %s numérique" % n)

4. if n < 0:

5. raise ValueError("On ne calcule pas de racines carrées sur %s négatif" % n)

6. return n ** 0.5 / n

7. try:

8. x=input("Entrez un nombre : ")

9. print(calcul(int(x)))

10. except (ValueError, ZeroDivisionError) as e:

11. print("Désolé, ce calcul est impossible sur %s– Exception %s" % (x, e))

XIX-5 - Assertion

L'assertion est un mécanisme permettant de contrôler la validité d’une expression avec l’instruction assert.

Si l’expression est vraie, l’instruction ne fait rien, sinon elle renvoie une exception AssertError qui peut être agrémentée d’un message personnalisé si celui‑ci est placé à la suite de l’instruction.

1. def inverse(n):

2. assert isinstance(n, (int, float)), "%s non numérique" % n

3. assert n!= 0, "Pas d’inverse possible pour 0"

4. return 1 / n

Le but de cette instruction est généralement de détecter les erreurs de programmation dans la phase de développement et non dans la phase d’utilisation. D’autant plus que l’exécution du programme en mode « optimisé » (option ‑O) ignore alors toutes les assertions présentes dans le code.

XIX-6 - Arbre des exceptions

L’ensemble possible des exceptions est organisé en arbre d’héritage à partir d’une exception racine BaseException.

Python 3

Python 2

BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
StopIteration
StopAsyncIteration
ArithmeticError
FloatingPointError
OverflowError
ZeroDivisionError
AssertionError
AttributeError
BufferError
EOFError
ImportError
ModuleNotFoundError
LookupError
IndexError
KeyError
MemoryError
NameError
UnboundLocalError
OSError
BlockingIOError
ChildProcessError
ConnectionError
BrokenPipeError
ConnectionAbortedError
ConnectionRefusedError
ConnectionResetError
FileExistsError
FileNotFoundError
InterruptedError
IsADirectoryError
NotADirectoryError
PermissionError
ProcessLookupError
TimeoutError
ReferenceError
RuntimeError
NotImplementedError
RecursionError
SyntaxError
IndentationError
TabError
SystemError
TypeError
ValueError
UnicodeError
UnicodeDecodeError
UnicodeEncodeError
UnicodeTranslateError
Warning
DeprecationWarning
PendingDeprecationWarning
RuntimeWarning
SyntaxWarning
UserWarning
FutureWarning
ImportWarning
UnicodeWarning
BytesWarning
ResourceWarning

BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
StopIteration
StandardError
BufferError
ArithmeticError
FloatingPointError
OverflowError
ZeroDivisionError
AssertionError
AttributeError
EnvironmentError
IOError
OSError
WindowsError (Windows)
VMSError (VMS)
EOFError
ImportError
LookupError
IndexError
KeyError
MemoryError
NameError
UnboundLocalError
ReferenceError
RuntimeError
NotImplementedError
SyntaxError
IndentationError
TabError
SystemError
TypeError
ValueError
UnicodeError
UnicodeDecodeError
UnicodeEncodeError
UnicodeTranslateError
Warning
DeprecationWarning
PendingDeprecationWarning
RuntimeWarning
SyntaxWarning
UserWarning
FutureWarning
ImportWarning
UnicodeWarning
BytesWarning

À noter que positionner une gestion d'exception sur l'exception X détectera aussi toutes les exceptions situées sous la branche X. De plus, les exceptions du plus haut niveau (situées au début de BaseException) intègrent les interruptions système comme le ctrl+c clavier (KeyboardInterrupt). Ainsi, le programmeur qui veut continuer à avoir la main sur son programme n'a pas vraiment intérêt à monter aussi haut. La première exception algorithmiquement utile pour lui est donc Exception dont toutes les autres exceptions pouvant survenir en programmation héritent. C’est aussi l’exception qui est prise par défaut quand elle n’est pas spécifiquement indiquée dans l’instruction Except.

XIX-7 - Définir ses propres exceptions

Les exceptions étant elles aussi des objets ; rien n'interdit alors d'en hériter pour définir ses propres exceptions.

1. class MyError(ValueError):

2. def __init__(self, v):

3. super().__init__()

4. self.__v=v

5. def __str__(self):

6. return "Erreur [%s]" % str(self.__v)

7. >>>

8. >>> raise MyError("toto")

9. __main__.MyError: Erreur [toto]

XIX-8 - Pardon ou permission

Généralement, le programmeur consciencieux a naturellement tendance à vérifier la possibilité des opérations avant de les exécuter ; plutôt que de voir survenir un souci et devoir le traiter (ou ne pas le traiter et laisser le code se crasher lamentablement).

Cependant, il existe des cas où cela n'est pas possible ou trop difficile. Celui qui veut par exemple ouvrir un fichier devra d’abord vérifier que le fichier existe, puis qu'il est du bon type, puis qu'il est accessible, etc. Et malgré tous les tests qu'il fera, il y aura toujours un risque potentiel que le fichier change d’état durant le laps de temps qu'il y aura eu entre les tests et l’ouverture réelle (OS multi-utilisateurs).

Devant les difficultés sous‑jacentes de cette démarche, le programmeur Python habitué préfèrera s'appuyer sur les mécanismes de protection intégrés au langage et lancer directement l’opération en présumant qu'elle fonctionnera ; tout en se protégeant des échecs éventuels en récupérant les exceptions associées pour les gérer. Bref, en Python, il vaut mieux demander « pardon » que « permission ».

XX - La programmation modulaire

XX-1 - Créer ses modules

La programmation modulaire permet de coder un projet Python qui s'appuiera sur plusieurs fichiers sources distincts.

D’une part, cela évite d’avoir un fichier source massif de plusieurs milliers de lignes qui effondrera le codeur sous sa masse et d’autre part, cela offre la possibilité de déporter le projet sur plusieurs codeurs spécifiques, chacun attaché à une partie du projet.

Un module est donc un script contenant une partie du projet. Une fois le module créé, il est alors accessible depuis un autre avec l'instruction import.

L'import peut soit se faire en local et dans ce cas le module est importé dans un scope à son nom ; soit se faire en global et dans ce cas le module est importé directement dans le scope global. Dans ce dernier cas, on doit spécifier explicitement le ou les sous-modules à importer, mais on peut utiliser l’étoile (*) pour signifier « tous les sous-modules disponibles ».

Exemple de module

Fichier « calculs.py »

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def sqrt(n): return n ** 0.5

5. def carre(n): return n ** 2

Exemple d'import local

Fichier « main.py »

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import calculs

5. print("La racine de 25 est %f et son carré est %d" % (calculs.sqrt(25), calculs.carre(25)))

Exemple d'import global

Fichier « main.py »

1. #!/usr/bin/env python3

2. # coding: utf-8

4. from calculs import *

5. print("La racine de 25 est %f et son carré est %d" % (sqrt(25), carre(25)))

Exemple d'import global spécifique

Fichier « main.py »

1. #!/usr/bin/env python3

2. # coding: utf-8

4. from calculs import sqrt, carre

5. print("La racine de 25 est %f et son carré est %d" % (sqrt(25), carre(25)))

La seconde méthode semble moins contraignante à l'utilisation. Toutefois, elle montrera ses limites lors de l’import de deux modules distincts contenant chacun des fonctions aux noms identiques. Dans ce cas, c'est le dernier importé qui gagne.

Importer deux fois le même module (ou importer un module « x » puis importer ensuite un module « y » lequel importe lui aussi le module « x ») n’a aucune conséquence (le second import est ignoré). Et importer un module dans le scope à la fois local et global (bien que peu utile et source d’ennuis) reste possible.

XX-2 - Tester ses modules

Le souci d'un import est qu'il importe tout le code du fichier importé. Y compris les lignes de tests éventuels qui seront alors exécutées à chaque fois que le fichier est importé.

Exemple

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def sqrt(n): return n ** 0.5

5. print(sqrt(25))                    # Test de vérification

Celui qui importera ce module verra systématiquement apparaître « 5.0 » à l'écran à chaque fois qu’il exécutera son programme. Ce qui, même si cela n'est pas néfaste, reste tout de même bien inutile.

Une façon de tester ses modules tout en évitant que ces tests s'exécutent lors de l'import consiste à utiliser le contenu de la variable __name__. En effet, ce contenu change selon que le fichier est exécuté directement (son contenu est alors __main__) ou importé depuis un autre script (son contenu est alors le nom du module).

Un simple test permet alors de détecter le cas et d'agir en conséquence…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def sqrt(n): return n ** 0.5

5. if __name__ == "__main__":

6. # Ce code ne s’exécutera pas dans le cas d’un import

7. print(sqrt(25))                # Test de vérification

Ici, celui qui exécute ce module verra le test s'effectuer tandis que celui qui l'importe ne verra rien de particulier.

XX-3 - Les membres implicites

Un module python possède quelques membres implicites et utilisables par le développeur :

- __builtins__ : contient les builtins (toutes les méthodes et objets) du module ;

- __doc__ : contient la première docstring (sera vu ultérieurement) du module ;

- __file__ : contient le nom et chemin du module ;

- __name__ : contient le nom du module quand il est importé ou la chaîne __main__ quand il est exécuté directement.

XX-4 - Import depuis un autre dossier

Python peut très bien importer un module se trouvant dans un sous-dossier.

XX-4-a - Import par le nom du module

Exemple à partir d’un dossier « xxx »

Fichier « xxx/calculs.py »

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def sqrt(n): return n ** 0.5

5. def carre(n): return n ** 2

Fichier « main.py » (import du nom complet du module « xxx.calculs.py »)

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import xxx.calculs

5. print("La racine de 25 est %f et son carré est %d" % xxx.calculs.sqrt(25), xxx.calculs.carre(25))

Fichier « main.py » (import du nom du module « calculs.py »par rapport à son dossier « xxx »)

1. #!/usr/bin/env python3

2. # coding: utf-8

4. from xxx import calculs

5. print("La racine de 25 est %f et son carré est %d" % calculs.sqrt(25), calculs.carre(25))

Fichier « main.py » (import du contenu entier du module « xxx.calculs.py »)

1. #!/usr/bin/env python3

2. # coding: utf-8

4. from xxx.calculs import *

5. print("La racine de 25 est %f et son carré est %d" % sqrt(25), carre(25))

XX-4-b - Import par le nom du dossier

Python pourra aussi importer un dossier directement par son nom à condition que ce dossier possède un fichier spécifique nommé « __init__.py ». Lors de l'import du dossier, ce fichier « __init__.py » est alors exécuté.

Exemple (suite du précédent)

Fichier « xxx/__init__.py »

1. #!/usr/bin/env python3

2. # coding: utf-8

3. import calculs

Fichier « main.py » (import du dossier « xxx » lequel fera importer par « __init__.py » le module « calculs.py » qui restera local à « xxx »)

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import xxx

5. print("La racine de 25 est %f et son carré est %d" % xxx.calculs.sqrt(25), xxx.calculs.carre(25))

Fichier « main.py » (import du contenu du dossier « xxx », lequel fera importer par « __init__.py » le module « calculs.py » qui sera alors global dans le code)

1. #!/usr/bin/env python3

2. # coding: utf-8

4. from xxx import *

5. print("La racine de 25 est %f et son carré est %d" % calculs.sqrt(25), calculs.carre(25))

À noter : les imports sont recherchés depuis une liste des dossiers stockée dans la variable sys.path. Cette liste contient entre autres un dossier vide ("") signifiant « dossier de l’exécutable appelé ».

XX-5 - Quelques modules utiles

XX-5-a - Modules natifs

Modules intégrés en standard dans la librairie Python :

- __future__ : bibliothèque de variables et outils qui ordonnent à une version Python de se comporter comme se comportera la version suivante sur certaines caractéristiques ;

- abc : bibliothèque de variables et d’outils fournissant une infrastructure pour définir des classes abstraites (Abstract Base Class) ;

- atexit : bibliothèque de variables et d’outils qui permettent d'activer des fonctions qui seront automatiquement appelées lorsque le programme se termine ou s'interrompt ;

- binascii et base64 : bibliothèques de variables et d’outils qui permettent de coder/décoder un flux binaire en représentation ASCII pour la première et en diverses représentations pour la seconde (la seconde est un enrobage enrichi de la première) ;

- builtins : bibliothèque contenant toutes les fonctions natives Python. Cela permet de créer sa propre fonction qui surchargera une fonction native et qui pourra donc appeler ladite fonction sans tomber dans un appel récursif ;

- collections : bibliothèque d’extensions spécifiques des containers de base (tuples, listes, ensembles et dictionnaires) ;

- configparser (anciennement ConfigParser dans Python 2) : bibliothèque de variables et d’outils qui permettent de gérer un fichier de configuration ;

- copy : bibliothèque de variables et d’outils permettant de dupliquer des objets Python ;

- csv : bibliothèque de variables et d’outils permettant de manipuler un fichier de type csv ;

- ctypes : bibliothèque de variables et d’outils permettant d'attaquer des modules écrits en C ;

- curses : bibliothèque de variables et d’outils permettant de gérer l'écran et son affichage ;

- dataclasses : bibliothèque de décorateurs qui peuvent créer automatiquement des méthodes standards (telles __init__() ou __repr__()) aux objets sur certaines spécifications ;

- datetime : bibliothèque de variables et d’outils qui permettent de gérer les dates et les heures ;

- decimal : bibliothèque de variables et d’outils qui permettent de gérer et calculer les nombres décimaux avec une précision absolue ;

- doctest : bibliothèque de variables et d’outils qui permettent de tester la documentation ;

- enum : bibliothèque de variables et d’outils qui permettent de créer des constantes symboliques (n'existe pas dans Python 2) ;

- fileinput : bibliothèque de variables et d’outils qui permettent de manipuler les fichiers standards (stdin, stdout et stderr) ;

- fractions : bibliothèque de variables et d’outils qui permettent de manipuler les fractions ;

- functools : bibliothèque de variables et d’outils qui permettent de manipuler les fonctions ;

- getopt : bibliothèque de variables et d’outils qui permettent de gérer les options format Unix ;

- grp : bibliothèque de variables et d’outils de manipulation de la base de données Unix gérant l’association entre groupe et gid (fichier « /etc/group ») ;

- hashlib : bibliothèque de variables et d’outils qui permettent de hasher (signer) un flux ;

- importlib : bibliothèque de variables et d’outils qui permettent d’importer des modules de façon dynamique ou paramétrable ;

- inspect : bibliothèque de variables et d’outils qui permettent de récupérer des informations sur les objets du code ;

- io : bibliothèque de variables et d’outils qui permettent de manipuler des descripteurs de fichiers (des flux IO) ;

- itertools : bibliothèque de variables et d’outils qui permettent de créer des itérateurs puissants ;

- json : bibliothèque de variables et d’outils qui permettent de manipuler des données au format JSON ;

- keyword : bibliothèque de variables et d’outils qui permettent de gérer les mots‑clef du langage ;

- logging : bibliothèque de variables et d’outils qui permettent de logguer des événements et actions ;

- math : bibliothèque de variables et d’outils qui permettent d'accéder à des fonctions mathématiques avancées ;

- numbers : classe de base abstraite numérique gérant la hiérarchie des nombres ;

- operator : contient toutes les fonctions correspondant aux opérateurs natifs, opérateurs pouvant alors être utilisables en tant qu’objets ;

- os : bibliothèque de variables et d’outils permettant d'accéder au système d'exploitation ;

- pathlib : bibliothèque représentant le système de fichiers avec la sémantique appropriée pour différents systèmes d'exploitation ;

- pdb : débogueur Python interactif ;

- pickle et _pickle (nommé cpickle dans Python 2) : bibliothèque de variables et d’outils qui permettent de sérialiser des objets Python. Le second _pickle (ou cpickle dans Python 2) correspond au premier, mais a été écrit en C pour être plus rapide ;

- pwd : bibliothèque de variables et d’outils de manipulation de la base de données Unix gérant l’association entre utilisateur et uid (fichier « /etc/passwd ») ;

- random : bibliothèque de variables et d’outils de génération de nombres aléatoires ;

- re : bibliothèque de variables et d’outils qui permettent d’utiliser des expressions régulières ;

- spwd : bibliothèque de variables et d’outils de manipulation de la base de données Unix gérant l’association entre utilisateur et mot de passe (fichier « /etc/shadow ») ;

- StringIO (Python 2 uniquement) : bibliothèque de variables et d’outils qui permettent de manipuler une chaîne comme un flux IO (intégré au module io dans Python 3) ;

- struct : bibliothèque de variables et d’outils qui permettent d’effectuer des conversions entre valeurs Python et structures binaires du C ;

- statistics : bibliothèque de variables et d’outils de calculs statistiques ;

- subprocess : bibliothèque de variables et d’outils permettant de lancer des programmes externes (des processus) et récupérer leur travail au travers de tubes de communication ;

- sys : bibliothèque de variables et d’outils permettant d'accéder au système dans lequel se déroule le script ;

- time : bibliothèque de variables et d’outils qui permettent de gérer les heures ;

- timeit : bibliothèque de variables et d’outils qui permettent d’effectuer des statistiques sur le temps d’exécution d’une fonction ;

- types : contient les types Python sous forme de variables. Dans Python 2, ce module contenait tous les types possibles (str, int, long, etc). Dans Python 3, il ne contient que les types qui ne sont pas déjà un objet existant (ex int n'est pas présent dans ce module, car l'objet int existe et peut être utilisé à sa place) ;

- typing : contient un support de prise en charge des annotations ;

- unicodedata : bibliothèque de variables et d’outils qui permettent de manipuler l’Unicode ;

- unittest : bibliothèque de variables et d’outils qui permettent d'automatiser des tests de fonctions et classes ;

- uuid : bibliothèque de variables et d’outils qui permettent de générer des UUID (Unique UID).

- xml : bibliothèque de variables et d’outils qui permettent de manipuler les fichiers xml.

- zlib : bibliothèque de variables et d’outils qui permettent de compresser ou décompresser un flux.

XX-5-b - Modules spécifiques

Modules nécessitant une installation spécifique, mais très souvent utilisés par nombre de développeurs Python :

- Cryptodome (anciennement nommé Crypto) : bibliothèque de variables et d’outils de chiffrement/déchiffrement selon différents algorithmes (Diffie-Hellman, RSA, etc.) ;

- matplotlib : bibliothèque de visualisation de fonctions et courbes mathématiques ;

- numpy : bibliothèque permettant la manipulation de matrices et tableaux multidimensionnels ;

- pandas : bibliothèque permettant la manipulation et l'analyse des données, tableaux numériques et séries temporelles ;

- psutil : bibliothèque de variables et d’outils de contrôle des processus et de charge système ;

- tkinter (nommé Tkinter dans Python 2) : bibliothèque de variables et d’outils qui permettent de créer des IHM graphiques minimalistes basée sur la librairie tcl/tk ;

- Pygtk : bibliothèque permettant de créer des IHM graphiques basées sur la librairie graphique GTK ;

- PyQt et PySide : bibliothèques permettant de créer des IHM graphiques basées sur la librairie graphique Qt. Le module PySide est une branche dérivée issue de PyQt ayant maintenant sa propre vie;

XXI - Pour tester les types

Le développeur Python pourra être amené parfois à devoir vérifier le type précis de ses variables. Cela peut être par exemple pour vérifier un paramètre reçu ou pour toutes autres raisons qui peuvent survenir dans la vie d’un projet.

Le premier réflexe sera alors d’utiliser la fonction type() pour comparer son résultat avec le type attendu.

Exemple :

1. def carre(n):

2. if type(n) not in (int, float):

3. print("Erreur %s n’est pas un type int/float" % n)

4. return None

5. return n**2

Mais cette façon de faire se heurtera très rapidement à deux problèmes :

- les chaînes peuvent être de plusieurs types (bytes ou str sous Python 3 et respectivement str ou unicode sous Python 2). Et dans chaque cas, les différents types ne sont pas (du point de vue Python) identiques ;

1. >>> type("xxx") == str

2. True

3. >>> type("xxx") == bytes

4. False

- les objets peuvent hériter les uns des autres. Un objet voiture pourra par exemple hériter d’un objet véhicule. Or, si d’un point de vue naturel, une voiture est bien un véhicule, d’un point de vue Python ce n’en est pas un.

1. >>> class vehicule: pass

2. ...

3. >>> class voiture(vehicule): pass

4. ...

5. >>> v=voiture()

6. >>> type(v) == voiture

7. True

8. >>> type(v) == vehicule

9. False

Pour pallier ces problèmes, Python offre la fonction isinstance().

Cette fonction permet de lui passer soit le type à vérifier (sous forme de paramètre unique), soit un ensemble de types à vérifier (sous forme de tuple). Elle renverra alors True/False si le type de l’élément correspond/ne correspond pas au type (ou à un des types) demandé(s).

1. def carre(n):

2. if not isinstance(n, (int, float)):

3. print("Erreur %s n’est pas un type int/float " % n)

4. return None

5. return n**2

1. >>> isinstance("xxx", str)

2. True

3. >>> isinstance(b"xxx", (str, bytes))

4. True

Cela semblera assez anodin comme possibilité (on aurait obtenu le même résultat avec type(v) in (typeX, typeY, typeZ)). Cependant, cette fonction offre le second avantage de pouvoir remonter l’héritage d’un objet. Et considèrera donc comme « vrai » si le type de l’objet correspond au type demandé ou à l’un de ses ascendants.

1. >>> class vehicule: pass

2. ...

3. >>> class voiture(vehicule): pass

4. ...

5. >>> class caravane(voiture): pass

6. ...

7. >>> c=caravane()

8. >>> isinstance(c, caravane)

9. True

10. >>> isinstance(c, voiture)

11. True

12. >>> isinstance(c, vehicule)

13. True

La fonction issubclass() est analogue à la fonction isinstance() à ceci près que la première ne peut tester qu’une variable ; ce qui oblige alors à instancier l’objet dans une variable si l’on veut le vérifier ; alors que issubclass() peut tester directement un objet (une classe) sans avoir à l’instancier.

1. >>> class vehicule: pass

2. ...

3. >>> class voiture(vehicule): pass

4. ...

5. >>> class caravane(voiture): pass

6. ...

7. >>> c=caravane()                       # Obligé d’instancier "caravane" dans une variable…

8. >>> isinstance(c, caravane)            # …pour pouvoir le vérifier avec "isinstance"

9. True

10. >>> issubclass(caravane, caravane)     # Alors qu’avec "issubclass" on n’est plus obligé

11. True

12. >>> issubclass(caravane, voiture)

13. True

14. >>> issubclass(caravane, vehicule)

15. True

16. >>> issubclass(voiture, caravane)

17. False

18. >>> issubclass(voiture, voiture)

19. True

20. >>> issubclass(voiture, vehicule)

21. True

22. >>> issubclass(vehicule, caravane)

23. False

24. >>> issubclass(vehicule, voiture)

25. False

26. >>> issubclass(vehicule, vehicule)

27. True

Et bien évidemment, tout comme isinstance(), la fonction issubclass() peut tester d'un seul appel plusieurs classes éventuelles (le test sera vrai si l’une des classes correspond).

1. >>> issubclass(vehicule, (caravane, voiture))

2. False

3. >>> issubclass(vehicule, (caravane, voiture, vehicule))

4. True

1. class LOTR:

2. @classmethod

3. def say(cls):

4. if issubclass(cls, Magicien):

5. print("Fuyez, pauvres fous... (et en plus c'est sans alcool) !!!")

6. if issubclass(cls, Nain):

7. print("Jamais je n'aurais pensé mourir aux côtés d'un elfe")

8. if issubclass(cls, Elfe):

9. print("Et que diriez-vous de mourir aux côtés d'un ami ?")

11. >>> class Magicien(LOTR): pass

12. ...

13. >>> class Nain(LOTR): pass

14. ...

15. >>> class Elfe(LOTR): pass

16. ...

17. >>> Magicien.say()

18. Fuyez, pauvres fous... (et en plus c’est sans alcool) !!!

19. >>> Nain.say()

20. Jamais je n’aurais pensé mourir aux côtés d’un elfe

21. >>> Elfe.say()

22. Et que diriez-vous de mourir aux côtés d’un ami ?

XXII - L’accès aux fichiers

L’accès aux fichiers concerne toute façon de communiquer des informations entre un fichier disque et le programme. Cette communication peut être entrante, sortante ou bidirectionnelle.

XXII-1 - L’accès au fichier

L’accès au fichier se fait par la fonction open(nomfichier, mode="r", buffering=-1; encoding=None). Le premier paramètre est le nom du fichier à ouvrir et le second sera le mode d’ouverture. On distingue 5 modes possibles :

- mode "r" : accès en lecture seule. C’est le mode par défaut. Le fichier doit nécessairement exister ;

- mode "w" : accès en écriture seule. Si le fichier n’existe pas, il est créé et s’il existe il est vidé ;

- mode "x" : accès en écriture, mais exclusivement en création. Le fichier doit nécessairement ne pas exister ;

- mode "a" : accès en écriture seule, mais en fin de fichier (mode « append »). Si le fichier n’existe pas, il est créé ;

- mode "r+" : comme "r", mais autorise l’écriture ;

- mode "w+" : comme "w", mais autorise la lecture ;

- mode "x+" : comme "x", mais autorise la lecture ;

- mode "a+" : comme "a", mais autorise la lecture ;

Ces modes peuvent en plus être agrémentés d’un "b" indiquant que la lecture ou l’écriture doivent se faire en mode binaire (ex "r+b"). Dans Python 2, ce mode n'a d'effet que sous Windows. En effet, Windows distingue ses fichiers textes par une suite de deux caractères ("\r\n") pour indiquer les fins de lignes. Et donc, toute écriture ou lecture en mode « non binaire » prendra en compte ces deux caractères ensemble comme représentant une fin de ligne ; alors que la lecture ou l’écriture en mode « binaire » ne les prendra pas en compte et les traitera l’un et l’autre de façon individuelle comme tout autre caractère.

Ce flag est aussi possible dans le monde Unix, mais n’aura d'effet que sous Python 3 permettant alors de différencier des fichiers textes (contenant des caractères encodés en utf-8) ou binaires (aucun encodage particulier).

Le troisième paramètre indique la façon de buffériser les entrées/sorties du fichier. Les valeurs possibles sont les suivantes :

- -1 : laisse le système gérer. C’est la valeur par défaut ;

- 0 : pas de bufférisation (toutes les IO sont synchrones) ;

- 1 : bufférisation par ligne (n’a de sens que pour les fichiers textes) ;

- Autre valeur positive : taille du buffer en octets (ne sera prise en compte que si elle est supérieure à 127 sinon elle est traitée comme 0).

Le quatrième paramètre (qui n'existe pas dans Python 2) permet de préciser l'encodage dans lequel est écrit le fichier. Mettre ce paramètre à None (qui est la valeur par défaut) permet de laisser la fonction le déterminer toute seule en analysant le fichier.

Dans Python 2, la fonction renvoie un objet file.

1. >>> fp=open("/etc/passwd", "r")

2. >>> type (fp)

3. <type 'file'>

Dans Python 3, la fonction renvoie un objet _io.BufferedReader si le fichier a été ouvert en mode binaire en lecture, un objet _io.BufferedWriter si le fichier a été ouvert en mode binaire en écriture ou un objet _io.TextIOWrapper sinon.

1. >>> rb=open("/etc/passwd", "rb")

2. >>> type (rb)

3. <class '_io.BufferedReader'>

5. >>> wb=open("/tmp/xxx", "wb")

6. >>> type (wb)

7. <class '_io.BufferedWriter'>

9. >>> r=open("/etc/passwd", "r")

10. >>> type (r)

11. <class '_io.TextIOWrapper'>

13. >>> w=open("/tmp/xxx", "w")

14. >>> type (w)

15. <class '_io.TextIOWrapper'>

Cette différence entre « binaire » et « texte » d’un côté et entre « lecture » et « écriture » de l’autre peut poser des soucis pour vérifier si un élément est ou n’est pas un fichier. Le module « io » contient un type IOBase commun permettant de simplifier ces vérifications.

1. >>> import io

3. >>> rb=open("/etc/passwd", "rb")

4. >>> isinstance(rb, io.IOBase)

5. True

7. >>> wb=open("/tmp/xxx", "wb")

8. >>> isinstance(wb, io.IOBase)

9. True

11. >>> r=open("/etc/passwd", "r")

12. >>> isinstance(r, io.IOBase)

13. True

15. >>> w=open("/tmp/xxx", "w")

16. >>> isinstance(w, io.IOBase)

17. True

Quoi qu’il en soit, c'est sur ces différents objets que se feront ensuite toutes les opérations de lecture et d’écriture (et pour simplifier, ils seront communément nommés file dans la suite du chapitre).

XXII-2 - Traitements sur le fichier

Les traitements sur le fichier se font à travers l’objet renvoyé par open() . Chaque opération de lecture ou d’écriture déplace l'élément interne de l’objet (on le nomme généralement « tête de lecture/écriture ») d’autant de caractères, ce qui permet de répéter les opérations pour traiter tout le fichier.

Les opérations possibles sont les suivantes :

- file.read(size=-1) : lecture de size octets. Si size vaut -1 (valeur par défaut), alors Python lit tout le fichier. La fonction renvoie une suite d’octets disponibles sous forme de str ou une chaîne vide s’il n’y a plus rien à lire ;

- file.readline(size=-1) : lecture de size octets. Si size vaut -1 (valeur par défaut), alors Python lit autant qu’il le peut, mais s’arrête dès qu’il rencontre une fin de ligne ("\n" sous Unix, "\r\n" sous Windows). La fonction renvoie une suite d’octets disponibles sous forme de str correspondant donc à une ligne du fichier ou une chaîne vide quand tout a été lu. Dans le monde Windows, si le fichier a été ouvert en mode « non binaire » ("r" et non "rb") alors la lecture transforme le double caractère fin de ligne ("\r\n") en une fin de ligne standard ("\n"). Cette fonction n’a de sens que pour une lecture de fichiers textes ;

- file.readlines(size=-1) : lecture de toutes les lignes jusqu’à concurrence de size octets. Si size vaut -1 (valeur par défaut), alors Python lit tout le fichier. La fonction renvoie une liste contenant toutes les lignes du fichier. Même remarque pour les fins de lignes éventuelles de ces chaînes. Cette fonction n’a de sens que pour une lecture de fichiers textes ;

- file.write(str) : écriture de la chaîne str dans le fichier. Attention, la fonction ne rajoute pas de fin de ligne. Si l’utilisateur veut que cette fin de ligne soit écrite, il doit alors la positionner ("\n") dans la chaîne à écrire. Dans le monde Windows, si le fichier a été ouvert en mode « non binaire » ("w" et non "wb") alors l’écriture transforme ce caractère ("\n") en une fin de ligne adaptée ("\r\n") ;

- file.writelines(iterable) : en considérant que l’itérable est un itérable de chaînes, la fonction écrit alors toutes ces chaînes dans le fichier. Même remarque pour les fins de lignes éventuelles de ces chaînes.

XXII-3 - Autres fonctions de manipulation

L’objet file contient d’autres opérations spécifiques possibles :

- file.tell() : renvoie la position en octets de la tête de lecture/écriture dans le fichier , position prise à partir du début du fichier ;

- file.seek(pos, whence=0) : positionne la tête de lecture/écriture à la position demandée. La position dépend du paramètre whence. S’il est à 0, alors la position est calculée à partir du début du fichier. S’il est à 1, alors la position est calculée à partir de la position courante. S’il est à 2, alors la position (qui sera exprimée en négatif) est calculée par rapport à la fin du fichier. Dans tous les cas, la position finale de la tête ne pourra pas dépasser le début ou la fin de fichier. À noter que la position de la tête de lecture/écriture est aussi modifiée par toute écriture sur un fichier ouvert en mode « append » (elle se place automatiquement à la fin du fichier avant d’effectuer l’écriture) ;

- file.truncate(size=-1) : coupe le fichier à la taille demandée. Si la taille vaut -1, alors coupe le fichier à la position actuelle de la tête de lecture/écriture. Si la taille demandée dépasse la taille initiale du fichier, alors le résultat dépend des plateformes (peut ne rien faire, peut rajouter des zéros ou peut rajouter des octets aléatoires). Le fichier doit avoir été ouvert en écriture (tous les modes sauf "r" ou "rb") ;

- file.flush() : vide le tampon d’écriture. Les octets sont alors réellement écrits sur le fichier disque. À noter que si la fonction open() a été demandée avec le paramètre buffer à 0 (IO synchrones), le tampon est alors toujours vide, car les données sont réellement écrites sur le disque à chaque instruction d’écriture. Cette fonction n’a de sens que pour les fichiers ouverts en écriture ;

- file.close() : ferme le fichier. Ça vide aussi son tampon d’écriture en finalisant les écritures restantes éventuelles.

XXII-4 - Itération

En plus de pouvoir y effectuer les opérations détaillées précédemment, un objet file est aussi un itérable où chaque itération renvoie une ligne du fichier (ce qui n’a de sens bien évidemment que pour les fichiers textes).

C’est-à-dire qu’on peut traiter un fichier texte ligne par ligne au travers d’une simple boucle sur l’objet file.

Exemple :

1. fp=open(fic, "r")

2. for lig in fp: print(lig)

3. fp.close()

Relativement équivalent à ceci :

1. fp=open(fic, "r")

2. for lig in fp.readlines(): print(lig)

3. fp.close()

La même chose de façon plus classique :

1. fp=open(fic, "r")

2. while True:

3. lig=fp.readline()

4. if not lig: break

5. print(lig)

6. fp.close()

XXIII - Le « context manager »

XXIII-1 - Préambule

Le « context manager » est un mécanisme qui garantit un nettoyage automatique dans le cas où le code quitte le bloc de travail de façon impromptue.

Prenons l’exemple d’un fichier contenant des nombres (un nombre par ligne) et d’un algorithme affichant chaque nombre inversé.

1. fp=open(fic, "r")

2. for lig in fp: print(1/int(lig))

3. fp.close()

Dans le cas où le fichier contient une ligne avec le nombre 0, une exception ZeroDivisionError est levée et remonte la chaîne des appels jusqu’à, si elle n’est pas récupérée, sortir du programme. Mais l'instruction de fermeture du fichier n’est alors jamais exécutée.

Dans ce cas précis, certains pourront ne pas s’en préoccuper, car le fichier est ouvert en lecture et de toute façon, il est quand même fermé par l’OS. Mais, d’une part, il s’agit d’une attitude peu professionnelle, et d’autre part, dans d’autres circonstances, cela peut-être bien plus catastrophique. En cas d’écriture par exemple, l’OS ne garantit pas la finalisation des écritures et tout n’est alors pas forcément écrit.

Par ailleurs, si l’exception ne remonte pas jusqu’à l’OS, mais qu’elle est récupérée dans une fonction en amont, la fonction n’a pas forcément connaissance qu’une ressource (fichier ouvert) avait été allouée…

1. def calcul():

2. res=0

3. fp=open("fic", "r")

4. for lig in fp: res+=1/int(lig)

5. fp.close()

6. return res

7. # calcul()

9. def traitement():

10. try:

11. print(calcul())

12. except ArithmeticError as e:

13. print("Le calcul n’a pas pu se faire…%s" % e)

14. # Et le fichier est resté ouvert !!!

15. # try

16. # traitement()

XXIII-2 - Utilisation

Une première solution sera de protéger le traitement du fichier par un bloc try pour pouvoir le fermer dans la clause finally et de protéger aussi l’appel à la fonction par un autre bloc try pour pouvoir gérer l’exception qui en remonte.

1. def calcul():

2. res=0

3. try:

4. fp=open("fic", "r")

5. for lig in fp: res+=1/int(lig)

6. finally:

7. fp.close()

8. # try

9. return res

10. # calcul()

12. def traitement():

13. try:

14. print(calcul())

15. except ArithmeticError as e:

16. print("Le calcul n’a pas pu se faire…%s" % e)

17. # Mais le fichier est au moins fermé !!!

18. # try

19. # traitement()

Cela fonctionne, mais donne un code assez lourd et maladroit. Deux try pour un seul travail et un autre développeur en collaboration, mais qui n’a pas forcément connaissance des détails, pourrait se demander pourquoi ce try sans except.

La seconde solution sera d’utiliser un context manager. On le met en place par la création d’un bloc with.

Le principe de sa syntaxe est de remplacer une instruction var=ressource() par with ressource() as var. Et comme il s’agit d’un bloc, il ne faut pas oublier les deux‑points terminant l’instruction.

Ensuite, on place dans le bloc toutes les instructions qui ont besoin d’accéder à la ressource. Et quoi qu’il se passe, Python garantit la libération de la ressource quand il quittera le bloc, et ce, quelle que soit la façon de le quitter.

1. def calcul():

2. res=0

3. with open ("fic", "r") as fp:

4. for lig in fp: res+=1/int(lig)

5. # with

6. # Ici, le fichier est automatiquement fermé – Même pas besoin de le demander

7. return res

8. # calcul()

10. def traitement():

11. try:

12. print(calcul())

13. except ArithmeticError as e:

14. print("Le calcul n’a pas pu se faire…%s" % e)

15. # Mais le fichier est quand même fermé !

16. # try

17. # traitement()

Et afin d’éviter la démultiplication des blocs (donc des tabulations avec les décalages de code qui en résultent) dans le cas de plusieurs ressources à suivre…

1. with open ("fic1", "r") as fp1:

2. with open("fic2", "r") as fp2:

3. print(

4. "ouvert %s %s" % (

5. "fp1" if not fp1.closed else "",

6. "fp2" if not fp2.closed else "",

7. )

8. )

9. # with

10. # Ici, le second fichier est automatiquement fermé

11. print(

12. "ouvert %s %s" % (

13. "fp1" if not fp1.closed else "",

14. "fp2" if not fp2.closed else "",

15. )

16. )

17. # with

18. # Ici, le premier fichier est automatiquement fermé

19. print(

20. "ouvert %s %s" % (

21. "fp1" if not fp1.closed else "",

22. "fp2" if not fp2.closed else "",

23. )

24. )

… il est possible de chaîner plusieurs with en utilisant la virgule.

1. with open ("fic1", "r") as fp1, open("fic2", "r") as fp2:

2. print(

3. "ouvert %s %s" % (

4. "fp1" if not fp1.closed else "",

5. "fp2" if not fp2.closed else "",

6. )

7. )

8. # with

9. # Ici, les deux fichiers sont automatiquement fermés

10. print(

11. "ouvert %s %s" % (

12. "fp1" if not fp1.closed else "",

13. "fp2" if not fp2.closed else "",

14. )

15. )

XXIII-3 - Création de son propre « context manager »

Il est bien évidemment possible de créer son propre context manager. Cela se fait par le biais d’un objet dans lequel le constructeur __init__() devra être prévu pour recevoir tous les paramètres habituellement passés lors de l’appel à ressource() et auquel on rajoute deux méthodes spécifiques :

- la méthode __enter__() : cette méthode est appelée lors de l’entrée dans le bloc with ;

- la méthode __exit__() : cette méthode est appelée lorsque le bloc with se termine.

Exemple : une réécriture simplifiée de la fonction open()

1. class myOpen:

2. # Constructeur

3. def __init__(self, name, mode="r"):

4. print("Création %s, %s " % (name, mode))

6. # Il faut mémoriser le nom et le mode, car l’objet en aura besoin pour l’ouvrir

7. (self.__name, self.__mode)=(name, mode)

8. # __init__()

10. # Quand le context manager demande la ressource

11. def __enter__(self):

12. print("Ouverture %s, %s " % (self.__name, self.__mode))

14. # Il faut mémoriser le fichier ouvert, car l’objet en aura besoin pour le fermer

15. self.__fp=open(self.__name, self.__mode)

17. # Ici on retourne le fichier ouvert. Il sera accessible avec "as"

18. return self.__fp

19. # __enter__()

21. # Quand le context manager quitte le bloc

22. def __exit__(self, tp, e, traceback):

23. print(

24. "Fermeture %s, %s - tp=%s, e=%s, traceback=%s" % (

25. self.__name, self.__mode,

26. tp, e, traceback,

27. )

28. )

30. # On ferme le fichier, opération qui était le but premier de ce context manager

31. self.__fp.close()

32. # __exit__()

33. # class myOpen

Et son utilisation :

1. with myOpen("/etc/passwd") as fp:

2. for lig in fp: print(lig)

Les paramètres tp, e et traceback sont automatiquement remplis quand le bloc with est interrompu par une exception. tp récupère le type de l’exception, e récupère le message d’erreur associé à l’exception et traceback récupère un objet traceback permettant de retracer manuellement le contenu de la pile des appels.

1. with myOpen("/etc/passwd") as fp:

2. raise IOError("erreur bye bye")

Autre exemple : revenir automatiquement dans le répertoire d’origine après s’être déplacé dans l’arborescence.

1. import os

3. class myCD:

4. def __init__(self, rep=os.getenv("HOME")):

5. # On mémorise le dossier demandé pour pouvoir y aller plus tard

6. self.__rep=rep

7. # __init__()

9. def __enter__(self):

10. # On mémorise le dossier courant pour pouvoir y revenir à la fin

11. self.__cwd=os.getcwd()

13. # Et on se déplace dans le dossier demandé

14. os.chdir(self.__rep)

15. # __enter__()

17. def __exit__(self, tp, e, traceback):

18. # On revient dans le dossier d'origine

19. os.chdir(self.__cwd)

20. # __exit__()

21. # class myCD

23. # On se déplace dans un dossier initial pour les tests

24. os.chdir("/")

25. print("dir0=%s" % os.getcwd())

27. with myCD("/tmp"):

28. print("dir1=%s" % os.getcwd())

29. with myCD("/var"):

30. print("dir2=%s" % os.getcwd())

31. with myCD():

32. print("dir3=%s" % os.getcwd())

33. # with

34. print("dir2=%s" % os.getcwd())

35. # with

36. print("dir1=%s" % os.getcwd())

37. # with

38. print("dir0=%s" % os.getcwd())

XXIV - Les décorateurs

XXIV-1 - Préambule

Le décorateur est un mécanisme permettant d’encapsuler une fonction quelconque dans une surcouche de traitements qui pourront s’exécuter avant ou après chaque appel à la fonction.

Cela permet de rajouter et enlever facilement des mécanismes d'optimisations (log, compteur d'appels, mise en mémoire des résultats les plus demandés) sur des fonctions du programme y compris sur des fonctions provenant d'une librairie externe et sur lesquelles on n’a aucune possibilité de modification.

Le principe du décorateur s’appuie sur le fait qu’une fonction, comme tout en Python, est avant tout un objet manipulable.

On peut donc par exemple l’assigner à une variable. Notez que dans ce cas, il n’y a pas de parenthèses au nom de la fonction, car on demande à récupérer l’objet fonction et non le résultat de son exécution.

1. >>> def bonjour(s, n=1):

2. ...    print("[%s]" % s * n)

3. ...

4. >>> salut=bonjour                            # Pas de parenthèses après le nom de fonction !

5. >>>

6. >>> bonjour("hello", 5)                      # On peut donc appeler la fonction…

7. [hello][hello][hello][hello][hello]

8. >>>

9. >>> salut("hello ", 5)                       # … tout comme on peut appeler sa copie

10. [hello][hello][hello][hello][hello]

On peut même supprimer la fonction d’origine vu qu’elle est référencée ailleurs, son bloc d’instructions ne sera pas supprimé.

1. >>> del bonjour

2. >>>

3. >>> bonjour("hello ", 5)

4. Traceback (most recent call last):

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

6. NameError: name 'bonjour' is not defined

7. >>>

8. >>> salut("hello ", 6)

9. [hello][hello][hello][hello][hello][hello]

Un second point à se rappeler est qu’une fonction peut être incluse à l’intérieur d’une autre fonction. Et que la fonction interne a quand même accès aux variables présentes dans la fonction externe…

1. >>> def bonjour(s, n=1):

2. ...    def maj():

3. ...        print("[%s]" % s.upper() * ((n//2))        # La fonction interne connait "n"

4. ...    maj()

5. ...    print("[%s]" % s * n)

6. ...

7. >>> bonjour("hello", 4)

8. [HELLO][HELLO]

9. [hello][hello][hello][hello]

Ces deux caractéristiques permettent alors par exemple à une fonction d’offrir un choix entre divers traitements, chaque choix étant dévolu à une fonction interne…

1. def bonjour(espece="humain"):

2. # On commence d’abord par créer les sous-fonctions des traitements offerts

3. def chien(n=1): print("[ouah]" * n)

4. def chat(n=1): print("[miaou]" * n)

5. def humain(n=1): print("[bonjour]" * n)

7. # Ensuite on retourne la sous-fonction associée à la caractéristique demandée

8. return {

9. "chien" : chien,

10. "chat" : chat,

11. "humain" : humain,

12. }[espece]

13. # bonjour()

15. >>> # Pour s’en servir, on récupère la sous-fonction qu’elle renvoie

16. >>> parler=bonjour("chien")

17. >>>

18. >>> # Et ensuite on peut appeler la sous-fonction récupérée

19. >>> parler(6)

20. [ouah][ouah][ouah][ouah][ouah][ouah]

21. >>>

22. >>> # On peut aussi faire le tout en une seule opération

23. >>> bonjour("chat")(5)

24. [miaou][miaou][miaou][miaou][miaou]

Le troisième point important pour comprendre les décorateurs est que puisqu’une fonction est un objet, on peut alors aussi la passer comme simple argument à une autre fonction qui aura alors la possibilité de l'exécuter à son gré.

1. def compter(n):

2. for i in range(1, n+1): print(i, end='')

4. def decorateur(fct, n):

5. print("Attention, la fonction %s va s'exécuter avec %d" % (fct, n))

6. fct(n)

7. print("Fonction %s terminée" % fct)

9. >>> decorateur(compter, 5)

10. Attention, la fonction <function compter at 0x7fb06479f5f0> va s’exécuter avec 5

11. 1 2 3 4 5

12. Fonction <function compter at 0x7fb06479f5f0> terminée

Ceci est donc l'exemple de base pour comprendre le décorateur : une surcouche qui ira exécuter du code avant et après la fonction qu’elle reçoit, tout en exécutant aussi la fonction qu’elle reçoit et sans que le développeur ait besoin de modifier la fonction en question.

XXIV-2 - Un premier décorateur

Prenons pour l’exemple une fonction assez simple : un calcul de factorielle…

1. >>> from functools import reduce                        # Inutile dans Python 2

2. >>> def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

3. ...

4. >>> print(facto(10))

5. 3628800

Le décorateur qui va décorer cette fonction factorielle doit alors recevoir ladite fonction afin de pouvoir l’encapsuler dans son propre traitement tout en lui faisant faire quand même son travail de factorielle.

Toutefois, et c'est là un point important, ce traitement ne doit pas se faire quand le décorateur est appelé, mais quand c'est la factorielle qui est appelée !

C'est pourquoi le décorateur est obligé de définir une fonction interne qui se chargera de la décoration proprement dite. Et le décorateur, lors de son appel, se contente de retourner cette fonction interne qui prendra alors la place de la factorielle d'origine.

Mais comme la fonction interne doit prendre la place de la factorielle, il est nécessaire qu'elle fasse le même travail que cette factorielle, donc qu'elle puisse appeler ladite factorielle et récupérer son résultat. En un mot qu’elle connaisse cette factorielle.

Ce qui donne le code suivant :

1. # Création d’un décorateur sur la fonction factorielle qu’il recevra en paramètre

2. def decorateur(fct):

3. # Le wrapper qui encapsule la factorielle dans un traitement spécifique

4. # Cette factorielle recevant un paramètre, il doit en faire de même

5. def wrapper(n):

6. # Il appelle ladite factorielle en lui passant à l’identique le paramètre reçu

7. # Notez que la variable "fct" connue du décorateur est donc connue du wrapper

8. r=fct(n)

10. # Un affichage quelconque pour l’exemple

11. print("Je décore %s (%s) - Le résultat est %s" % (fct, n, r))

13. # Il renvoie le résultat calculé par la factorielle

14. return r

15. # wrapper()

17. # Ici on est dans le décorateur. Il va alors simplement renvoyer le wrapper

18. return wrapper

19. # decorateur()

Ensuite, il ne reste plus qu’à remplacer la factorielle initiale par le wrapper renvoyé par le décorateur.

1. >>> # D'abord création de la fonction factorielle

2. >>> from functools import reduce

3. >>> def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

4. ...

5. >>> # Ensuite remplacement de la factorielle par le wrapper renvoyé par le décorateur

6. >>> facto=decorateur(facto)

7. >>>

8. >>> # Enfin test de la fonction décoréé

9. >>> facto(10)

10. Je décore <function facto at 0x7f2693b26bf8> (10) - Le résultat est 3628800

11. 3628800

On a donc bien finalement le message provenant du wrapper, ainsi que le résultat de la fonction d’origine (appelée en réalité par le wrapper, mais ce détail reste transparent pour l’utilisateur). C’est un décorateur.

Arrivé ici, Python offre aussi un sucre syntaxique au travers de la syntaxe @decorateur qui remplace l’instruction fonction=decorateur(fonction).

Ainsi, le code précédent devient :

1. >>> # Création de la fonction factorielle directement décorée

2. >>> from functools import reduce

3. >>> @decorateur

4. ... def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

5. ...

6. >>> # Ensuite test de la fonction décorée

7. >>> facto(10)

8. Je décore <function facto at 0x7f2693b26bf8> (10) - Le résultat est 3628800

9. 3628800

Bien entendu, un même décorateur peut s'appliquer sur différentes fonctions qui auraient la même signature (ici un paramètre)…

1. # Ce même décorateur appliqué sur une fonction carre()

2. @decorateur

3. def carre(n): return n**2

5. # Ce même décorateur appliqué sur une fonction cube()

6. @decorateur

7. def cube(n): return n**3

9. >>> carre(10)

10. Je décore <function carre at 0x7f2bb0b81950> (10) - Le résultat est 100

11. 100

12. >>> cube(10)

13. Je décore <function cube at 0x7f2bb0ab3e18> (10) - Le résultat est 1000

14. 1000

… et une même fonction peut être décorée par différents décorateurs.

1. from functools import reduce

3. # Fonction factorielle décorée plusieurs fois (vous écrirez ces décorateurs vous‑mêmes)

4. @decorateurA

5. @decorateurB

6. @decorateurC

7. def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

XXIV-3 - Un peu plus complexe : un décorateur plus universel

Ce premier décorateur possède l'inconvénient majeur de ne pouvoir être appliqué qu'à une fonction recevant un seul argument et échouera à décorer une fonction plus complexe…

1. # Application du même décorateur sur une fonction somme() à 2 paramètres

2. @decorateur

3. def somme(a, b): return a+b

5. >>> somme(5, 6)

6. Traceback (most recent call last):

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

8. TypeError: wrapper() takes 1 positional argument but 2 were given

9. >>>

Toutefois, rappelons qu'il existe deux types de paramètres très particuliers *args et **kwargs qui permettent d'adapter une fonction à toute combinaison d'arguments reçus.

Exemple d’un second décorateur qui devient universel, car il ne se préoccupe plus des arguments envoyés à la fonction, il les lui passe tels quels :

1. # Le décorateur sur la fonction

2. def decorateur(fct):

3. # Le wrapper qui encapsule la fonction dans un traitement spécifique

4. # Il récupère en vrac tous les arguments passés à la fonction

5. def wrapper(*args, **kwargs):

6. # Il appelle la fonction en lui passant à l’identique les arguments reçus

7. r=fct(*args, **kwargs)

9. # Un affichage quelconque

10. print("Je décore %s (%s, %s) - Le résultat est %s" % (fct, args, kwargs, r))

12. # Il renvoie le résultat calculé par la fonction

13. return r

14. # wrapper()

16. # Ici on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction

17. return wrapper

18. # decorateur()

20. # Application sur une fonction recevant un argument

21. from functools import reduce

22. @decorateur

23. def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

25. >>> facto(10)

26. Je décore <function facto at 0x7f6cd7405ea0> ((10,), {}) - Le résultat est 3628800

27. 3628800

29. # Application sur une fonction recevant deux arguments

30. @decorateur

31. def somme(a, b): return a+b

33. >>> somme(5, 6)

34. Je décore <function somme at 0x7f6cd7337ea0> ((5, 6), {}) - Le résultat est 11

35. 11

37. # Et on peut nommer les arguments, ils passent alors par kwargs

38. >>> somme(b=5, a=6)

39. Je décore <function somme at 0x7f6cd7337ea0> ((), {"a" : 6, "b" : 5}) - Le résultat est 11

40. 11

XXIV-4 - Le danger du décorateur

Une fois qu’on a compris ce principe, on commence à imaginer toute une gamme de possibilités. Cependant, le décorateur présente un danger si on l’associe à une fonction récursive. N’oublions pas que la fonction est intégralement remplacée par le décorateur. L’appel récursif entrainera alors l’appel du décorateur de façon récursive.

Reprenons l’exemple de la factorielle, mais en la codant de façon récursive…

1. # Création de la fonction factorielle de façon récursive

2. @decorateur

3. def facto(n): return n * facto(n-1) if n else 1

5. >>> facto(5)

6. Je décore <function facto at 0x7f99bdde0668> ((0,), {}) - Le résultat est 1

7. Je décore <function facto at 0x7f99bdde0668> ((1,), {}) - Le résultat est 1

8. Je décore <function facto at 0x7f99bdde0668> ((2,), {}) - Le résultat est 2

9. Je décore <function facto at 0x7f99bdde0668> ((3,), {}) - Le résultat est 6

10. Je décore <function facto at 0x7f99bdde0668> ((4,), {}) - Le résultat est 24

11. Je décore <function facto at 0x7f99bdde0668> ((5,), {}) - Le résultat est 120

12. 120

Le résultat du programme n’est peut-être pas ce que voulait le concepteur du décorateur. Ou celui que voulait l’utilisateur du décorateur.

Pour supprimer ce désagrément, il faut veiller à ce que toute fonction récursive soit toujours encapsulée dans une couche principale non récursive. Ce point est expliqué plus en détail au chapitre sur l'optimisation d'une fonction récursive. Ainsi, c'est la couche principale qui sera décorée et non la fonction récursive…

1. # Création de la fonction factorielle de façon récursive

2. @decorateur

3. def facto(n):

4. # La sous-fonction moteur du calcul récursif

5. calcul_r=lambda n: n * calcul_r(n-1) if n else 1

7. # Renvoi calcul récursif de la factorielle

8. return calcul_r(n)

9. # facto()

11. >>> facto(5)

12. Je décore <function facto at 0x7f99bdde0668> ((5,), {}) - Le résultat est 120

13. 120

XXIV-5 - Exemple : un décorateur qui gère les appels déjà connus

Exemple d’un décorateur qui va optimiser les appels. Tout appel déjà connu ne sera alors pas recalculé.

Pour pouvoir stocker le tableau des appels, on se sert du fait que le décorateur est une fonction et qu’une fonction est aussi un objet sur lequel on peut rajouter des attributs spécifiques.

1. #!/usr/bin/env python3

2. # coding: utf-8

4. # Un décorateur qui va optimiser les appels

5. # Tout appel déjà connu évite alors de recalculer le résultat

6. def optimize(fct):

7. # Création du stockage des résultats (fixé à 3 pour l'exemple)

8. # C'est une liste de dictionnaires contenant les arguments de la fonction et son résultat    result=[{"n" : None, "r" : None},] * 3

10. # Le wrapper qui encapsule la fonction dans un traitement spécifique

11. # Il récupère en vrac tous les arguments passés à la fonction

12. def wrapper(*args, **kwargs):

13. # Si les arguments sont déjà connus (donc dans l’un des dictionnaires de la liste)

14. for x in result:

15. if x["n"] == (args, kwargs):

16. # Récupération du résultat (qui est aussi dans le même dictionnaire)

17. r=x["r"]

18. print("Résultat de (%s, %s) déjà connu => %s" % (args, kwargs, r))

19. break

20. # if

21. else:

22. # Le résultat n'est pas encore connu - Il est alors calculé

23. r=fct(*args, **kwargs)

24. print("Calcul de (%s, %s) - Le résultat est %s" % (args, kwargs, r))

26. # On supprime le premier résultat pour pouvoir enregistrer le dernier

27. del result[0]

28. result.append({"n" : (args, kwargs), "r" : r})

29. # for

31. # Affichage du tableau des résultats (pour démo)

32. print("\n".join(str(x) for x in result))

34. # Renvoi du résultat (qui a été récupéré ou calculé)

35. return r

36. # wrapper()

38. # Ici, on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction

39. return wrapper

40. # optimize()

42. from functools import reduce

43. @optimize

44. def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

46. # Exemple de calcul avec des valeurs dupliquées

47. for i in (7, 5, 9, 5, 9, 1, 7, 9):     print("facto(%d)=%d" % (i, facto(i)), end="\n\n")

Et le résultat…

1. Calcul de ((7,), {}) - Le résultat est 5040

2. {'n': None, 'r': None}

3. {'n': None, 'r': None}

4. {'n': ((7,), {}), 'r': 5040}

5. facto(7)=5040

7. Calcul de ((5,), {}) - Le résultat est 120

8. {'n': None, 'r': None}

9. {'n': ((7,), {}), 'r': 5040}

10. {'n': ((5,), {}), 'r': 120}

11. facto(5)=120

13. Calcul de ((9,), {}) - Le résultat est 362880

14. {'n': ((7,), {}), 'r': 5040}

15. {'n': ((5,), {}), 'r': 120}

16. {'n': ((9,), {}), 'r': 362880}

17. facto(9)=362880

19. Résultat de ((5,), {}) déjà connu => 120

20. {'n': ((7,), {}), 'r': 5040}

21. {'n': ((5,), {}), 'r': 120}

22. {'n': ((9,), {}), 'r': 362880}

23. facto(5)=120

25. Résultat de ((9,), {}) déjà connu => 362880

26. {'n': ((7,), {}), 'r': 5040}

27. {'n': ((5,), {}), 'r': 120}

28. {'n': ((9,), {}), 'r': 362880}

29. facto(9)=362880

31. Calcul de ((1,), {}) - Le résultat est 1

32. {'n': ((5,), {}), 'r': 120}

33. {'n': ((9,), {}), 'r': 362880}

34. {'n': ((1,), {}), 'r': 1}

35. facto(1)=1

37. Calcul de ((7,), {}) - Le résultat est 5040

38. {'n': ((9,), {}), 'r': 362880}

39. {'n': ((1,), {}), 'r': 1}

40. {'n': ((7,), {}), 'r': 5040}

41. facto(7)=5040

43. Résultat de ((9,), {}) déjà connu => 362880

44. {'n': ((9,), {}), 'r': 362880}

45. {'n': ((1,), {}), 'r': 1}

46. {'n': ((7,), {}), 'r': 5040}

47. facto(9)=362880

À noter : ce décorateur existe déjà dans le module functools et se nomme functools.lru_cache.

1. >>> from functools import lru_cache

2. >>> @lru_cache(maxsize=10)

3. >>> def facto(n): return n * facto(n-1) if n else 1

5. >>> print(facto(8))                # Toutes les valeurs de 0 à 8 sont calculées

6. 40320

7. >>> print(facto(5))                # Valeur déjà calculée, ne sera donc pas recalculée

8. 120

9. >>> print(facto(10))               # Seules les valeurs 9 et 10 seront calculées

10. 3628800

XXIV-6 - Encore plus complexe : donner des arguments au décorateur

L’exemple précédent utilise un tableau des appels défini arbitrairement à 3 éléments par le créateur du décorateur. Ce qui limite un peu son utilisation dans des configurations diverses comme cela arrive souvent dans le monde du développement collaboratif et hétérogène. Peut-être que certains utilisateurs qui travaillent sur des ordinateurs puissants aimeraient pouvoir optimiser le calcul sur 10, 100, 1000, 10000 résultats…

C'est possible en décorant le décorateur lui‑même. Ainsi, en encapsulant le décorateur qui gère les résultats de la fonction (qu'on nommera décorateurFct) dans un autre décorateur (nommé décorateurParam), ce dernier peut alors gérer des arguments donnés par l’utilisateur. Ainsi, les arguments donnés à décorateurParam pourront servir à paramétrer décorateurFct.

Exemple : paramétrer le tableau de résultats. Le décorateur recevra comme paramètre la taille allouée au tableau (qui pourra toutefois être à 0 s'il décide de ne pas optimiser) ainsi qu'un booléen de contrôle indiquant si l’on doit afficher ou pas le tableau…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. # Le décorateur qui récupère des paramètres de décoration demandés par l'appelant

5. def optimize(size=0, debug=False):

6. # Création du stockage des résultats (la taille étant donnée par l'appelant)

7. result=[{"n" : None, "r" : None},] * size

9. # Le décorateur qui optimise la fonction

10. def __optimizeFct(fct):

11. # Le wrapper qui gère tous les schémas d'arguments possibles

12. def __wrapper(*args, **kwargs):

13. # Récupération du résultat si déjà connu

14. # Notez l'utilisation d'un "tuple en intension" créé à partir du tableau de résultats

15. # Cela permet alors d'y appliquer la méthode "index()" à la volée

16. # Cette méthode "index()" renverra une exception si le résultat n'est pas connu

17. try:

18. idx=tuple(x["n"] for x in result).index((args, kwargs))

19. r=result[idx]["r"]

20. print("Résultat de (%s, %s) déjà connu => %s" % (args, kwargs, r))

21. except ValueError:

22. # Le résultat n'est pas encore connu - Il est alors calculé

23. r=fct(*args, **kwargs)

24. print("Calcul de (%s, %s) - Le résultat est %s" % (args, kwargs, r))

26. # Si l'utilisateur a défini une taille d'optimisation

27. if size:

28. del result[0]

29. result.append({"n" : (args, kwargs), "r" : r})

30. # if

31. # try

33. # Si l’utilisateur veut tracer le tableau résultats

34. if debug: print("\n".join(str(x) for x in result) if size else "{}")

36. # Renvoi du résultat

37. return r

38. # __wrapper()

40. # Ici, on est dans le décorateur dédié à la fonction - Renvoi wrapper fonction

41. return __wrapper

42. # __optimizeFct()

44. # Ici, on est dans le décorateur dédié aux arguments – Renvoi décorateur fonction

45. return __optimizeFct

46. # optimize()

48. from functools import reduce

49. @optimize(size=5, debug=False)        # L'utilisateur veut optimiser sur 5 calculs

50. def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

XXIV-7 - L'objet décorateur

Ce mécanisme de décoration à deux niveaux commence à devenir complexe (3 couches d’imbrications). L'utilisation d'un objet pour créer un décorateur permet de le simplifier en gérant et organisant les divers éléments (arguments du décorateur, arguments de la fonction, etc.). En effet, la création de l'objet permettra de gérer ses arguments et son appel permet de gérer la fonction (car souvenons-nous qu'un objet peut être appelable et c'est lors de l'appel du décorateur qu'on remplace la fonction initiale par le wrapper du décorateur).

Le même exemple avec un objet décorateur…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. # Un objet décorateur qui va optimiser les appels

5. # Tout appel déjà connu évite alors de recalculer le résultat

6. class cOptimize:

7. # À la création du décorateur, on traite ses paramètres

8. def __init__(self, size=0, debug=False):

9. self.__result=list(({"n" : None, "r" : None},) * size)

10. self.__debug=debug

11. # __init__()

13. # À l’appel du décorateur, on traite la fonction qu'il décore

14. def __call__(self, fct):

15. # Le wrapper de la fonction

16. def wrapper(*args, **kwargs):

17. # Gestion résultat déjà connu

18. try:

19. idx=tuple(x["n"] for x in self.__result).index((args, kwargs))

20. r=self.__result[idx]["r"]

21. print("Résultat de (%s, %s) déjà connu => %s" % (args, kwargs, r))

22. except ValueError:

23. r=fct(*args, **kwargs)

24. print("Calcul de (%s, %s) - Le résultat est %s" % (args, kwargs, r))

25. if self.__result:

26. del self.__result[0]

27. self.__result.append({"n" : (args, kwargs), "r" : r})

28. # if

29. # try

30. if self.__debug:

31. print("\n".join(str(x) for x in self.__result) if self.__result else "{}")

33. # Renvoi du résultat

34. return r

35. # wrapper()

37. # On est revenu à l’appel du décorateur - Renvoi wrapper fonction

38. return wrapper

39. # __call__()

40. # class cOptimize

42. from functools import reduce

43. @cOptimize(size=5, debug=False)

44. def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

46. # Ou alors (écriture originelle)

47. def facto(n): return reduce(lambda x, y: x * y, range(1, n+1)) if n else 1

48. facto=cOptimize(size=7, debug=True)(facto)

XXIV-8 - Conclusion

De par sa souplesse d’utilisation, le décorateur se place comme un outil assez fondamental dans le développement d’applications Python. Il permet de programmer des compteurs d’appels, des optimiseurs, des debuggueurs, des logs, des chronomètres, des vérificateurs d'arguments, etc. Et tout cela sans avoir à modifier les fonctions à décorer qui peuvent même provenir de librairies externes et ne sont par conséquent pas modifiables.

XXIV-9 - Dernier détail…

Le décorateur ayant remplacé la fonction originelle, celle-ci perd toute son identité jusqu'à son nom.

Reprenons l'exemple du premier décorateur de ce chapitre…

1. # Le décorateur sur la fonction

2. def decorateur(fct):

3. # Le wrapper qui encapsule la fonction dans un traitement spécifique

4. def wrapper(*args, **kwargs):

5. # Il renvoie juste le résultat calculé par la fonction

6. return fct(*args, **kwargs)

7. # wrapper()

9. # Ici on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction

10. return wrapper

11. # decorateur()

… et regardons ce qui se passe quand la fonction décorée veut afficher son identité :

1. # Création d'une fonction quelconque

2. @decorateur

3. def somme(a, b):

4. print(somme)                # La fonction affiche son identité (son nom quoi)

5. return a+b

7. >>> somme(2, 3)

8. <function decorateur.<locals>.wrapper at 0x7fd5acdf9b70>

9. 5

Cette identité n'est plus celle de la fonction d'origine somme(), mais celle du wrapper qui l'a remplacée.

Le module functools (déjà vu dans ce chapitre) contient un décorateur wraps permettant à la fonction décorée de garder son identité.

Il suffit de rajouter l'instruction @wraps() en y donnant la fonction décorée pour qu'elle conserve son identité dans le décorateur.

Exemple :

1. from functools import wraps

3. # Le décorateur sur la fonction

4. def decorateur(fct):

5. # Tout d'abord, on conserve l'identité de la fonction décorée

6. @wraps(fct)

7. # Ensuite on écrit le wrapper qui va décorer la fonction

8. def wrapper(*args, **kwargs):

9. # Ceci est juste un exemple pour montrer "@wraps"

10. # Ce décorateur ne fait donc rien et renvoie simplement le résultat de la fonction

11. return fct(*args, **kwargs)

12. # wrapper()

14. # Ici on est dans le décorateur. Il va alors renvoyer le wrapper sur la fonction

15. return wrapper

16. # decorateur()

… et regardons la différence quand la fonction décorée veut afficher son identité…

1. # Création d'une fonction quelconque

2. @decorateur

3. def somme(a, b):

4. print(somme)                # La fonction affiche son identité

5. return a+b

7. >>> somme(2, 3)

8. <function somme at 0x7fd5acdf9b70>

9. 5

… et voilà. Tout est devenu transparent pour tout le monde.

XXV - La précision des nombres décimaux

La précision des nombres décimaux est un souci lié non pas au langage Python, mais à l’informatique dans son ensemble.

Ce souci est directement issu de la méthode employée par un processeur pour convertir un nombre décimal en binaire.

En effet, tout nombre décimal est découpé en sommes de fractions de 1/2n (n montant de façon naturelle). Ainsi 0.8125 est en fait converti en 1/2^1 + 1/2^2 + 1/2^4, ce qui donne 0.1101 en binaire.

Mais cette méthode ne fonctionne en réalité avec exactitude que sur très peu de nombres décimaux et donc pour beaucoup d’autres, leur fait perdre de la précision. Par exemple, 0.6 sera un tout petit peu plus grand que 1/2^1 + 1/2^4 + 1/2^5, mais un tout petit peu plus petit que 1/2^1 + 1/2^4 + 1/2^5 + 1/2^6 et un tout petit peu plus grand que 1/2^1 + 1/2^4 + 1/2^5 + 1/2^8 + 1/2^9, mais un tout petit peu plus petit que 1/2^1 + 1/2^4 + 1/2^5 + 1/2^8+ 1/2^9+ 1/2^10, etc. Et aucune somme de fractions de 1/2n (n entier) ne pourra tomber exactement sur 0.6.

Python peut arriver à compenser en général l’erreur d’imprécision à l’affichage.

1. >>> 0.6

2. 0.6

Mais cela n’est pas garanti dans toutes les situations…

1. >>> 0.6/3

2. 0.19999999999999998

3. >>> 0.2*3

4. 0.60000000000000001

5. >>>

… ce qui peut causer des soucis si la précision d’un nombre ou des calculs sur ce nombre est impérative au chiffre près (gestion de comptes bancaires par exemple).

Python possède cependant un module decimal offrant des outils de gestion et de manipulation des nombres décimaux avec une précision absolue. Le principe de ce module est de travailler le nombre décimal chiffre par chiffre, comme à l’école primaire. Ainsi 0.2*3 sera calculé chiffre par chiffre ('0', '.', '2') multiplié par '3' (avec la retenue si nécessaire, etc.).

1. >>> import decimal

2. >>> decimal.Decimal("0.6") / 3

3. Decimal('0.2')

4. >>> decimal.Decimal("0.2") * 3

5. Decimal('0.6')

6. >>>

Cela se paye toutefois en retour par un temps de calcul très important. Tout comme pour le principe d’incertitude de Heinsenberg, on ne peut pas avoir à la fois la précision et la vitesse.

XXVI - Divers

XXVI-1 - Python Enhancement Proposals (PEP)

Les PEP (Python Enhancement Proposals) sont des propositions d’améliorations du langage et de sa bibliothèque standard. On y trouve aussi des conseils d’écriture pour obtenir un code facilement lisible. On trouvera par exemple les conseils suivants (liste non exhaustive) :

- mettre des espaces avant et après les opérateurs de comparaison (if a == b et non if a==b) ;

- mettre des espaces dans les affectations (a = None et non a=None) ;

- mettre des espaces avant et après le deux‑points séparant les clefs des valeurs dans un dictionnaire (d = {1 : "toto"} et non d = {1:"toto"}) ;

- mettre un espace après la virgule (tp = (1, 2, 3) et non tp = (1,2,3)) ;

- ne pas écrire de lignes de plus de 80 caractères. On peut parfaitement découper des instructions mêmes complexes sur plusieurs lignes (quitte à protéger la fin de ligne, symbolisant l’exécution de l’action, par un backslash inhibiteur…) ;

Incorrect

1. var=("Pim", "Pam", "Poum")

2. if var[0] == "Pim" and var[1] == "Pam" and var[2] == "Poum" :

3. …

4. string="La raison du plus fort est toujours la meilleure"

Correct

1. var=(

2. "Pim",

3. "Pam",

4. "Poum",

5. )

6. if var[0] == "Pim"\

7. and var[1] == "Pam"\

8. and var[2] == "Poum" :

9. …

10. string="La raison du plus fort"\

11. + " est toujours la meilleure."

- ne pas mettre de virgule après le dernier élément d’un itérable (tp = (1, 2, 3) et non tp = (1, 2, 3,)), excepté pour les itérables écrits sur plusieurs lignes ou les tuples n’ayant qu’un seul élément, car là, c’est la virgule qui fait le tuple (tp = (1,)) ;

- même pour les fonctions à une instruction, préférer les définitions de fonctions explicites aux lambdas ;

Incorrect

1. carre=lambda x : x*x

Correct

1. def carre(x) : return x*x

- limiter les gestions d’exceptions aux seules instructions pouvant amener l’exception plutôt que regrouper plusieurs instructions sans pouvoir distinguer ensuite laquelle a provoqué l’exception ;

Incorrect

1. try:

2. return fct(collection[key])

3. except KeyError:

4. return key_not_found(key)

Correct

1. try:

2. value=collection[key]

3. except KeyError:

4. return key_not_found(key)

5. else:

6. return fct(value)

- expliciter le return None implicite.

Incorrect

1. def racine(x):

2. if x >= 0: return x**0.5

Incorrect

1. def racine(x):

2. if x < 0: return

3. return x**0.5

Correct

1. def racine(x):

2. if x >= 0: return x**0.5

3. return None

Correct

1. def racine(x):

2. if x < 0: return None

3. return x**0.5

XXVI-2 - Zen de Python

Le Zen de Python est un ensemble de 19 principes qui influencent la conception du langage de programmation Python et sont utiles pour comprendre et utiliser le langage.

Écrit et publié sur la liste de discussion de Python en juin 1999 par Tim Peters, le Zen de Python a été ensuite publié comme le PEP 203, il est aussi présent (en anglais) sur le site web officiel de Python. Il est également inclus comme « Easter egg » dans la distribution de l'interpréteur Python et apparait quand on tape la commande import this dans la console Python.

Beauté vaut mieux que laideur.
Explicite vaut mieux qu'implicite.
Simple vaut mieux que complexe.
Complexe vaut mieux que compliqué.
Linéaire vaut mieux qu’imbriqué.
Aéré vaut mieux que dense.
La lisibilité compte.
Les cas particuliers ne le sont pas assez pour enfreindre les règles.
Mais, à la pureté, privilégie l'aspect pratique.
Les erreurs ne doivent jamais être passées sous silence.
Sauf si explicitement réduites au silence.
Face à l'ambiguïté, refusez la tentation de deviner.
Il devrait y avoir une, et de préférence une seule, manière évidente de le faire.
Bien que cela ne soit pas évident immédiatement, à moins que vous ne soyez néerlandais(2).
Mieux vaut maintenant que jamais.
Mais jamais est souvent mieux qu'immédiatement.
Si la mise en œuvre est difficile à expliquer, c'est une mauvaise idée.
Si la mise en œuvre est facile à expliquer, cela peut être une bonne idée.
Les espaces de noms sont une excellente idée, il faut les utiliser.

XXVI-3 - Les docstrings

Les docstrings sont un mécanisme permettant d’automatiser les tests unitaires et la documentation du code qui sont, comme tout développeur le sait, les tâches les plus pénibles de son métier.

Là encore, la philosophie Python entre en jeu, car moins il y a de freins, plus il y a de chances qu’on le fasse.

Le principe de la docstring est d’encadrer un texte par trois guillemets doubles (") accolés.

Exemple :

1. def ajouter(a, b):

2. """ Ajoute deux nombres l'un à l'autre et retourne le résultat. """

4. return a + b

En dehors de l’avantage principal que de pouvoir comprendre facilement ce que fait telle ou telle fonction simplement en ouvrant le script, la docstring est aussi lue par Python pour pouvoir générer l’aide associée à la fonction lors de l’appel à help().

1. >>> help(ajouter)

2. Help on function ajouter in module __main__:

3. ajouter(a, b)

4. Additionne deux nombres et retourne le résultat.

La docstring peut s’étendre sur plusieurs lignes, être intégrée aux classes et aussi aux méthodes…

1. class StarTrek:

2. """

3. Cette classe vous donne accès à l’univers StarTrek

4. Un univers sans limites.

5. """

7. def where_no_one_has_gone_before(self):

8. """

9. Vous envoie là où personne n’est allé avant vous

10. """

11. pass

Une demande d’aide sur l’objet se traduira par l’affichage de sa docstring…

1. >>> help(StarTrek)

2. Help on class StarTrek in module __main__:

4. class StarTrek(__builtin__.object)

5. |  Cette classe vous donne accès à l’univers StarTrek

6. |  Un univers sans limites.

7. |

8. |  Methods defined here:

9. |

10. |  where_no_one_has_gone_before(self)

11. |    Vous envoie là où personne n’est allé avant vous

… ainsi qu’une demande d'aide sur l’une de ses méthodes.

1. >>> help(StarTrek.where_no_one_has_gone_before)

2. Help on method where_no_one_has_gone_before in module __main__:

4. where_no_one_has_gone_before(self) unbound b.StarTrek method

5. Vous envoie là où personne n’est allé avant vous

XXVI-4 - La bonne façon de faire ses tests

Il y a plusieurs façons de tester les éléments dans Python, façons qui se traduisent par autant de symboles voire de mots clefs distincts (=, is, in, etc.). Toutes fonctionneront dans la quasi-totalité des situations. Mais toutes ne seront pas aussi efficaces selon la nature de l’élément testé.

Pour tester une valeur concrète, on utilisera les opérateurs usuels de relation d’ordre (« égal à », « différent de », etc.).

1. >>> var=5

2. >>> if var <= 5: print("ok")

3. …

4. ok

Pour tester une valeur booléenne, on utilisera directement la valeur dans la condition. Éventuellement précédée de not si nécessaire.

1. >>> var1=True

2. >>> if var1: print("ok")

3. …

4. ok

5. >>> var2=False

6. >>> if not var2: print("ok")

7. …

8. ok

Pour tester la valeur spéciale None, on utilisera l’opérateur is indiquant l’état. Car d'une part None n’est pas une valeur, c’est un état et d'autre part l'opérateur is est le seul opérateur qui ne puisse pas être surchargé.

On peut bien entendu ne pas suivre ces préconisations et utiliser par exemple is pour une égalité (ex if var is 5). Mais d'une part Python devra pour cela faire des transformations implicites qui ralentiront le processus et d'autre part cela peut parfois ne pas fonctionner.

Exemple de comparaison d'une instance de classe avec None :

1. >>> class toto:

2. …    def __eq__(self, x): return True

3. …    def __ne__(self, x): return False

4. …

5. >>> toto() == None

6. True

7. >>> toto() != None

8. False

Comme les opérateurs de comparaison ont été réécrits, le résultat donne n'importe quoi. Alors que si l’on utilise la bonne façon de faire…

1. >>> toto() is None

2. False

3. >>> toto() is not None

4. True

… le résultat est correct.

Exemple d'évaluation booléenne d'une chaîne non vide (donc considérée comme True) :

1. >>> var="Hello"

2. >>> if var is True: print("ok")

3. …

4. >>> if var == True: print("ok")

5. …

6. >>>

Même si une chaîne non vide est « considérée » comme vraie, elle n'est pas strictement égale à True. Et donc ces tests ne donnent pas le résultat attendu. Alors qu'en employant la méthode adéquate d'une évaluation booléenne…

1. >>> if var: print("ok")

2. …

3. ok

… le résultat est là aussi correct.

XXVI-5 - Différences entre Python 2 et Python 3

Voici une liste non exhaustive des différences entre Python 2 et Python 3

Python 2

Python 3

Type int

Valeur numérique entière pouvant aller approximativement de -2^62 à 2^62

Valeur numérique entière virtuellement infinie

Type long

Valeur numérique entière virtuellement infinie

N’existe plus

Opérateur slash (/)

Division selon le type le plus précis des opérandes impliqués. Une division impliquant un flottant sera exacte, mais une division de deux entiers sera entière

Division exacte

Instruction print

Peut s’utiliser comme une instruction ou comme une fonction

Devenue fonction, obligation de mettre des parenthèses

Instruction yield from iterable

N’existe pas

Crée un générateur sur chaque élément de l’itérable

Une chaîne xxx ou r"xxx"

Est encodée en ASCII et est du type str

Est encodée en Unicode et est du type str

Une chaîne b"xxx"

Est encodée en ASCII et est du type str

Est encodée en ASCII et est du type bytes

Une chaîne f"xxx"

N’existe pas

Permet d’insérer des variables ou des expressions directement dans la chaîne

Une chaîne u"xxx"

Est encodée en Unicode et est du type unicode

Est encodée en Unicode et est du type str

Fonction bytes()

N’existe pas

Renvoie une chaîne encodée en ASCII

Fonction cmp()

Compare ses arguments et retourne une valeur numérique indiquant si l’argument de gauche est plus petit, égal ou plus grand que l’argument de droite

N’existe plus

Fonction input()

Permet de faire saisir une valeur qui sera typée selon sa syntaxe de saisie

Permet de faire saisir une valeur qui sera typée str

Fonction map()

Renvoie une liste

Renvoie un objet map

Fonction range()

Renvoie une liste

Renvoie un objet range

Fonction raw_input()

Permet de faire saisir une valeur qui sera typée str

N’existe plus

Fonction reduce()

Est une fonction native

A été déportée dans le module functools

Fonction str()

Renvoie une chaîne encodée en ASCII

Renvoie une chaîne encodée en Unicode

Fonction unicode()

Renvoie une chaîne encodée en Unicode

N'existe plus

Fonction xrange()

Renvoie un générateur

N’existe plus

Méthodes dict.keys(), dict.values() et dict.items()

Renvoient des listes

Renvoient des objets dict_keys, dict_values et dict_items (ce sont des objets dynamiques qui se synchronisent automatiquement en cas d'évolution du dictionnaire)

Méthodes dict.iterkeys(), dict.itervalues() et dict.iteritems()

Renvoient des objets dictionary_keyiterator, dictionary_valueiterator et dictionary_itemiterator (ce sont des générateurs qui sont donc n’utilisables qu’une seule fois et qui renvoient une exception RuntimeError si le dictionnaire est modifié entre leur création et leur utilisation)

N’existent plus

Méthodes dict.viewkeys(), dict.viewvalues() et dict.viewitems()

Renvoient des objets dict_keys, dict_values et dict_items (ce sont des objets dynamiques qui se synchronisent automatiquement en cas d'évolution du dictionnaire)

N’existent plus

Méthode generator.next()

Renvoie l’élément suivant du générateur

N’existe plus

Méthode list.clear()

N’existe pas

Vide la liste

Méthode object.__bool__()

N’existe pas

Est utilisée quand on demande bool(object)

Méthode object.__bytes__()

N’existe pas

Est utilisée quand on demande bytes(object)

Méthode object.__cmp__()

Permet de définir une méthode de comparaison générale

N’existe plus

Méthode object.__div__() et object.__rdiv__()

Permettent de redéfinir la division dans un objet

N’existent plus

Méthode object.__nonzero__()

Est utilisée quand on demande bool(object)

N’existe plus

Méthode object.__truediv__() et object.__rtruediv__(

N’existent pas

Permettent de redéfinir la division exacte dans un objet

Méthode object.__unicode__()

Est utilisée quand on demande unicode(object)

N’existe plus

Paramètre *args

Doit être placé en dernière position des paramètres ou en avant-dernière si le paramètre **kwargs (qui, lui, sera impérativement placé en dernier) est présent

Peut être placé à n’importe quelle position des paramètres à l'exception de la dernière si le paramètre **kwargs (qui, lui, sera impérativement placé en dernier) est présent

Argument cmp de la fonction sort()

Permet de spécifier une fonction qui aura pour charge de comparer deux éléments à trier

N'existe plus. Son fonctionnement est reproduit par l'objet cmp_to_key() du module functools, objet qui convertit une fonction en clef, et qu'on passera à l'argument key

exec et print

Sont des mots clefs

Sont des mots sans signification particulière

nonlocal

Est un mot sans signification particulière

Est un mot clef

None, True et False

Sont des variables globales

Sont des mots clefs

L'héritage de l'objet primitif object

Doit être explicitement demandé

Est intégré en natif dans les objets de l'utilisateur

Fonction super()

Doit contenir impérativement deux arguments : le type de l’objet dans lequel elle est appelée et l’instance de l’objet qui l’appelle (self)

Les arguments type et instance sont facultatifs

XXVII - Exemples

XXVII-1 - Suite n-bonacci

La suite N‑bonacci est une construction mathématique basée sur la suite de Fibonacci mais avec un nombre N d’éléments de départ ; et où chaque terme sera alors la somme des N termes qui le précèdent.

Par exemple la suite 4‑bonacci partira de 4 chiffres (ex U0=1, U1=1, U2=1, U3=1) et chaque terme suivant sera alors la somme des 4 termes précédents (U4=4, U5=7, U6=13, etc).

Si on prend la suite N‑bonacci, comme référence, alors la suite de Fibonacci est simplement une suite N‑bonacci de rang 2 (une 2‑bonacci).

Petit exemple d’une suite de N‑bonacci où la base de départ est donnée par l’appelant.

XXVII-1-a - En récursif

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def n_bonacci(base, n=0):

5. if n < len(base): return base[n]

6. return sum(n_bonacci(base, n - i) for i in range(1, len(base) + 1))

7. # n_bonacci()

9. if __name__ == "__main__":

10. for i in range(11):

11. print(i, n_bonacci((1, 2, 3, 4), i))

12. # if

XXVII-1-b - En itératif

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def n_bonacci(base, n=0):

5. if n < len(base): return base[n]

6. for i in range(n - len(base) + 1):

7. base=base[1:] + (sum(base),)

8. return base[-1]

9. # n_bonacci()

11. if __name__ == "__main__":

12. for i in range(11):

13. print(i, n_bonacci((1, 2, 3, 4), i))

14. # if

XXVII-1-c - Avec une pile

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def n_bonacci(base, n=0):

5. res=0

6. pile=[n,]

7. while pile:

8. v=pile.pop()

9. if v >= len(base):

10. pile.extend(range(v-1, v-len(base)-1, -1))

11. else:

12. res+=base[v]

13. # while

14. return res

15. # n_bonacci()

17. if __name__ == "__main__":

18. for i in range(11):

19. print(i, n_bonacci((1, 2, 3, 4), i))

20. # if

XXVII-1-d - Sous forme de générateur

1. #!/usr/bin/env python3

2. # coding: utf-8

4. def n_bonacci(base, n=0):

5. for i in range(n):

6. if i < len(base):

7. yield base[i]

8. continue

9. # if

10. base=base[1:] + (sum(base),)

11. yield base[-1]

12. # for

13. # n_bonacci()

15. if __name__ == "__main__":

16. for (i, f) in enumerate(n_bonacci((1, 2, 3, 4), 11)):

17. print(i, f)

18. # if

XXVII-2 - Tracer le scope local

Création d’un objet spécifique avec affichage dans les méthodes __init__() et __del__() pour montrer le mécanisme de création et suppression automatique des variables locales dans une fonction.

1. # class cTrace()

2. def __init__(self, n):

3. print("init:", n)

4. self.__n=n

5. def __del__(self): print("del:", self.__n)

6. # class cTrace

XXVII-2-a - Quand l’objet est stocké dans une variable de la fonction

1. def fct(n):

2. print("in fct:", n)

3. var=cTrace(n)

4. print("out fct:", n)

6. print(1)                  # Message témoin

7. fct("xxx")                # Appel de la fonction

8. print(2)                  # Message témoin

Résultat :

1. 1

2. in fct: xxx

3. init: xxx

4. out fct: xxx

5. del: xxx

6. 2

Explications : le premier message « in fct » montre la fonction qui démarre avec son paramètre reçu. Ensuite, elle instancie un objet cTrace qui est récupéré par la variable var. Le message « init » est donc affiché à l'instanciation de l'objet. Puis la fonction se termine et affiche le message « out fct » avant de sortir. Lors de cette sortie, ses variables internes sont alors toutes supprimées ce qui détruit l'objet par ricochet et le message « del » de l'objet est alors affiché à sa destruction. Et tout ceci se passe entre les deux messages témoins.

XXVII-2-b - Quand l’objet est créé à la volée sans être récupéré par une variable

1. def fct(n):

2. print("in fct:", n)

3. cTrace(n)

4. print("out fct:", n)

6. print(1)                  # Message témoin

7. fct("xxx")                # Appel de la fonction

8. print(2)                  # Message témoin

Résultat :

1. 1

2. in fct: xxx

3. init: xxx

4. del: xxx

5. out fct: xxx

6. 2

Explications : la différence se fait ici lors de l'instanciation de l'objet. Cette instance n'étant pas récupérée dans une variable, l'objet est alors immédiatement détruit juste après avoir été créé et ce, avant que la fonction ne se termine.

XXVII-3 - Tracer le fonctionnement des paramètres par défaut

Création d’un objet spécifique avec affichage dans les méthodes __init__() et __del__() pour montrer le mécanisme de création des valeurs par défaut des paramètres d’une fonction.

1. #!/usr/bin/env python3

2. # coding: utf-8

4. class cTrace:

5. def __init__(self): print("init: ", self)

6. def __del__(self): print("del: ", self)

7. # class cTrace

9. # Première définition de fonction avec un paramètre ayant l’objet comme valeur par défaut

10. def fct(n=cTrace()): print("fct:", n)

12. # Seconde définition de fonction qui va écraser la première

13. def fct(n=cTrace()): print("fct:", n)

15. print(1)                  # Message témoin

16. fct("essai")              # Appel avec argument concret (donc pas d’argument par défaut)

17. print(2)                  # Message témoin

18. fct()                     # Appel avec argument par défaut

19. print(3)                  # Message témoin

… et au résultat :

1. init: <__main__.cTrace object at 0x7f00e496d470>

2. init: <__main__.cTrace object at 0x7f00e496d550>

3. del: <__main__.cTrace object at 0x7f00e496d470>

4. 1

5. fct: essai

6. 2

7. fct: <__main__.cTrace object at 0x7f00e496d550>

8. 3

9. del: <__main__.cTrace object at 0x7f00e496d550>

Explications : le premier message « init » provient de l’objet cTrace() créé lorsque la fonction est définie. On a déjà là la démonstration que l’argument par défaut est créé à la définition de la fonction et non à son appel. Ensuite, le second message « init » provient d’un second objet cTrace() créé lorsque la fonction est de nouveau définie. À ce moment‑là, la première fonction disparait et avec elle les objets qu’elle a créés, d’où le premier message « del ». Viennent ensuite, le message témoin « 1 » puis le message en provenance de la fonction, laquelle affiche le paramètre reçu. Puis s’affichent le second message témoin « 2 » puis de nouveau le message en provenance de la fonction, laquelle affiche cette fois le paramètre par défaut (objet cTrace créé auparavant). Arrive enfin le troisième message témoin « 3 ». Puis le programme s’arrête détruisant alors la fonction fct ce qui détruit par ricochet son argument par défaut.

XXVII-4 - Tracer le fonctionnement des fonctions incluses

Création d’un objet spécifique avec affichage dans les méthodes __init__() et __del__() pour montrer le mécanisme de création des fonctions incluses mis en œuvre lors de l’appel (et non de l’écriture) de la fonction parente.

1. #!/usr/bin/env python3

2. # coding: utf-8

4. class cTrace:

5. def __init__(self): print("init: ", self)

6. def __del__(self): print("del: ", self)

7. # class cTrace

9. # Définition de la fonction parente

10. def fct():

11. # Fonction incluse avec un paramètre ayant l’objet comme valeur par défaut

12. def xxx(t=cTrace()): pass

13. print("fct")

14. # fct()

16. print(1)                  # Message témoin

17. fct()                     # Appel fonction

18. print(2)                  # Message témoin

19. fct()                     # Appel fonction

20. print(3)                  # Message témoin

…et au résultat :

1. 1

2. init:  <__main__.cTrace object at 0x7f12be52f470>

3. fct

4. del:  <__main__.cTrace object at 0x7f12be52f470>

5. 2

6. init:  <__main__.cTrace object at 0x7f12be52f470>

7. fct

8. del:  <__main__.cTrace object at 0x7f12be52f470>

9. 3

Explications : affichage tout d’abord du message témoin « 1 » puis d’un premier « init » provenant de l’objet cTrace() créé lorsque l’appel de la fonction fct() ce qui entraine la création de la fonction xxx() (qui, elle, n’a pas besoin d’être appelée). Puis exécution du premier « del » lorsque la fonction xxx() est détruite et avec elle ses objets. Puis affichage du message témoin « 2 » puis appel de nouveau à la fonction fct() entrainant la création de la fonction xxx() et de son objet en tant que paramètre par défaut, puis nouvel appel de la fonction fct() entrainant la destruction de son contenu et donc de xxx() et de ses objets par ricochet puis enfin, affichage du message témoin « 3 ».

XXVII-5 - Fonction « cmp_to_key() »

Reproduction de la fonction cmp_to_key() du module « functools » permettant de passer une fonction comme critère de tri à sorted().

1. # La fonction test va trier alphabétiquement chaque chaîne inversée ("abc" vu comme "cba")

2. def compare(x, y):

3. if x[::-1] < y[::-1]: return -1

4. if x[::-1] > y[::-1]: return 1

5. return 0

7. # La fonction cmp_to_key qui convertit une fonction en clef

8. # Pour cela, elle crée un objet interne contenant tous les opérateurs relationnels

9. # Chaque opérateur utilisera la fonction à convertir pour faire ses évaluations

10. # La fonction cmp_to_key n’a plus alors qu’à renvoyer l’objet

11. # De là, toute comparaison de deux éléments par sort() passera alors par l’objet

12. def my_cmp_to_key(fct):

13. class __cFct:

14. def __init__(self, k): self.__k=k

15. def __eq__(self, other): return fct(self.__k, other.__k) == 0

16. def __ne__(self, other): return fct(self.__k, other.__k) != 0

17. def __lt__(self, other): return fct(self.__k, other.__k) < 0

18. def __le__(self, other):     return fct(self.__k, other.__k) <= 0

19. def __gt__(self, other): return fct(self.__k, other.__k) > 0

20. def __ge__(self, other): return fct(self.__k, other.__k) >= 0

21. return __cFct

23. >>> auteurs=("Hugo", "Racine", "Balzac")

24. >>> for x in sorted(auteurs, key=my_cmp_to_key(compare), reverse=True): print(x)

25. ...

26. Hugo

27. Racine

28. Balzac

30. >>> from functools import cmp_to_key

31. >>> for x in sorted(auteurs, key=cmp_to_key(compare), reverse=True): print(x)

32. ...

33. Hugo

34. Racine

35. Balzac

XXVII-6 - Petit benchmark sur les différentes façon de copier une liste

Il a été vu lors du chapitre sur la copie des objets mutables, qu’une liste pouvait être copiée soit par l’opérateur Python slice ([:]), soit par la création d’une nouvelle liste à travers le mot‑clef list(), soit par la méthode copy() de l’objet liste lui‑même. Y a‑t‑il une différence entre ces trois façon de faire ?

Voici un petit exemple de benchmark des trois méthodes…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import random

5. import timeit

6. from functools import partial

8. # Initialisation random

9. random.seed()

11. # Les fonctions à tester

12. fct={

13. "slice" : lambda l: l[:],

14. "copy" : lambda l: l.copy(),

15. "list" : lambda l: list(l),

16. }

18. # Les données à traiter

19. data=list(range(10_000))

21. # Le nombre de répétitions (les moyennes se feront sur cette valeur)

22. repeat=20

24. # Appel des fonctions dans un ordre aléatoire et affichage du chrono

25. print("start=%d, repeat=%d" % (len(data), repeat))

26. for (k, v) in random.sample(fct.items(), len(fct)):

27. t=timeit.Timer(partial(v, data)).repeat(repeat=repeat, number=500_000)

28. print("%s: min=%f, max=%f, avg=%f" % (k, min(t), max(t), sum(t)/len(t)))

29. # for

Et (au bout d’environ 12 minutes) un résultat…

1. start=10000, repeat=20

2. copy: min=12.088104, max=12.497785, avg=12.270556

3. list: min=11.166771, max=11.717188, avg=11.329839

4. slice: min=12.093280, max=12.523560, avg=12.241432

À ce qu’il semble, c’est le cast list() qui serait le plus prometteur…

XXVII-7 - Petit benchmark sur le append() face aux listes en compréhension

Il a été vu lors du chapitre sur les listes en comprehension, qu’en dehors d’une écriture plus concise, son avantage principal était d’éviter la méthode list.append() présentée comme globalement très consommatrice.

Voici un petit exemple de benchmark permettant de comparer les deux écritures…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import random

5. import timeit

6. from functools import partial

8. # Initialisation random

9. random.seed()

11. # La fonction qui crée la liste par append()

12. def fct_append(l):

13. res=list()

14. for x in l: res.append(x)

15. # fct_append()

17. # La fonction qui passe par les listes en compréhension

18. def fct_comprehension(l): return list(x for x in l)

20. # Les fonctions à tester (on évitera la lambda pour être le plus impartial possible)

21. fct={

22. "append" : fct_append,

23. "comprehension" : fct_comprehension,

24. }

26. # Les données à traiter

27. data=tuple(range(50_000_000))

29. # Le nombre de répétitions (les moyennes se feront sur cette valeur)

30. repeat=10

32. # Appel des fonctions dans un ordre aléatoire et affichage du chrono

33. print("start=%d, repeat=%d" % (len(data), repeat))

34. for (k, v) in random.sample(fct.items(), len(fct)):

35. t=timeit.Timer(partial(v, data)).repeat(repeat=repeat, number=20)

36. print("%s: min=%f, max=%f, avg=%f" % (k, min(t), max(t), sum(t)/len(t)))

37. # for

Et (au bout d’environ 10 minutes) un résultat…

1. start 50000000, repeat=10

2. append: min=33.960994, max=34.511193, avg=34.186071

3. comprehension: min=27.848069, max=28.732663, avg=28.312427

Là en revanche, les écarts sont clairs et avérés. Près de 18 % plus rapide avec l’écriture en compréhension (ou près de 22 % plus lent avec la méthode list.append())…

XXVII-8 - Benchmark sur le map() face aux listes en compréhension

Il a été vu lors du chapitre sur les compléments des tuples, listes, ensembles et dictionnaires, qu’il existait une fonction map() permettant de faire traiter tout un itérable par une fonction, la fonction étant alors appliquée sur chaque élément de l’itérable.

Guido Van Rossum, le concepteur de Python, a dit qu’il n’aimait pas la fonction map(), car on peut facilement la remplacer par une liste en compréhension plus rapide et plus lisible. Et beaucoup de sites sur Internet ont repris ces deux arguments.

Sur la lisibilité d’une liste en compréhension face à la fonction map(), cela reste uniquement une affaire de goûts dont on ne peut pas discuter. En revanche, on peut parfaitement discuter de la rapidité.

Toutefois, n’oublions pas qu’on peut faire traiter l’itérable (on dira « liste » pour simplifier) par une fonction lambda ou bien par une fonction déjà existante. De plus, il ne faut pas oublier que la fonction map() peut traiter plusieurs listes en parallèle…

XXVII-8-a - Liste unique

Voici un petit exemple de benchmark permettant de comparer les deux écritures quand on traite une liste unique…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import random

5. import timeit

6. from functools import partial

8. # Initialisation random

9. random.seed()

11. # Une fonction somme

12. def somme(x): return x

14. # Les données à traiter

15. data=list(range(10_000))

17. # Les fonctions à tester

18. fct={

19. "with_lambda" : {

20. "map" : lambda l: list(map(lambda x: x, l)),

21. "comprehension" : lambda l: [x for x in l],

22. },

23. "with function" : {

24. "map" : lambda l: list(map(somme, l)),

25. "comprehension" : lambda l: [somme(x) for x in l],

26. },

27. }

29. # Le nombre de répétitions (les moyennes se feront sur cette valeur)

30. repeat=20

32. # Appel des groupes

33. print("start=%d, repeat=%d" % (len(data), repeat))

34. for (k, v) in fct.items():

35. # Appel des fonctions du groupe dans un ordre aléatoire et affichage du chrono

36. for (kk, vv) in random.sample(v.items(), len(v)):

37. t=timeit.Timer(partial(vv, data)).repeat(repeat=20, number=10_000)

38. print("%s (%s): min=%f, max=%f, avg=%f" % (kk, k, min(t), max(t), sum(t)/len(t)))

39. # for

40. # for

Et (au bout d’environ 4 minutes) un résultat…

1. start=10000, repeat=20

2. map (with_lambda): min=4.058490, max=4.133878, avg=4.078926

3. comprehension (with_lambda): min=1.302672, max=1.342291, avg=1.310797

4. map (with function): min=2.902643, max=3.067246, avg=2.937640

5. comprehension (with function): min=4.787955, max=5.248774, avg=4.841857

XXVII-8-b - Deux listes

Voici un petit exemple de benchmark permettant de comparer les deux écritures quand on traite deux listes en parallèle. Mais dans le cas de la compréhension, il faudra associer les listes au travers de la fonction zip()…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import random

5. import timeit

6. from functools import partial

8. # Initialisation random

9. random.seed()

11. # Une fonction somme

12. def somme(x, y): return x+y

14. # Les données à traiter

15. data=list(range(10_000))

17. # Les fonctions à tester

18. fct={

19. "with_lambda" : {

20. "map" : lambda l: list(map(lambda x, y: x+y, l, l)),

21. "comprehension" : lambda l: [x+y for (x, y) in zip(l, l)],

22. },

23. "with function" : {

24. "map" : lambda l: list(map(somme, l, l)),

25. "comprehension" : lambda l: [somme(x, y) for (x, y) in zip(l, l)],

26. },

27. }

29. # Le nombre de répétitions (les moyennes se feront sur cette valeur)

30. repeat=20

32. # Appel des groupes

33. print("start=%d, repeat=%d" % (len(data), repeat))

34. for (k, v) in fct.items():

35. # Appel des fonctions du groupe dans un ordre aléatoire et affichage du chrono

36. for (kk, vv) in random.sample(v.items(), len(v)):

37. t=timeit.Timer(partial(vv, data)).repeat(repeat=20, number=10_000)

38. print("%s (%s): min=%f, max=%f, avg=%f" % (kk, k, min(t), max(t), sum(t)/len(t)))

39. # for

40. # for

Et (au bout d’environ 8 minutes) un résultat…

1. start=10000, repeat=20

2. map (with_lambda): min=6.430227, max=6.563590, avg=6.527753

3. comprehension (with_lambda): min=4.760782, max=4.880451, avg=4.794313

4. comprehension (with function): min=8.747528, max=8.944287, avg=8.803548

5. map (with function): min=5.686407, max=5.852362, avg=5.749310

XXVII-8-c - Trois listes

Voici un petit exemple de benchmark permettant de comparer les deux écritures quand on traite trois listes en parallèle (avec bien entendu toujours cette association au travers de la fonction zip())"…

1. #!/usr/bin/env python3

2. # coding: utf-8

4. import random

5. import timeit

6. from functools import partial

8. # Initialisation random

9. random.seed()

11. # Une fonction somme

12. def somme(x, y, z): return x+y+z

14. # Les données à traiter

15. data=list(range(10_000))

17. # Les fonctions à tester

18. fct={

19. "with_lambda" : {

20. "map" : lambda l: list(map(lambda x, y, z: x+y+z, l, l, l)),

21. "comprehension" : lambda l: [x+y+z for (x, y, z) in zip(l, l, l)],

22. },

23. "with function" : {

24. "map" : lambda l: list(map(somme, l, l, l)),

25. "comprehension" : lambda l: [somme(x, y, z) for (x, y, z) in zip(l, l, l)],

26. },

27. }

29. # Le nombre de répétitions (les moyennes se feront sur cette valeur)

30. repeat=20

32. # Appel des groupes

33. print("start=%d, repeat=%d" % (len(data), repeat))

34. for (k, v) in fct.items():

35. # Appel des fonctions du groupe dans un ordre aléatoire et affichage du chrono

36. for (kk, vv) in random.sample(v.items(), len(v)):

37. t=timeit.Timer(partial(vv, data)).repeat(repeat=20, number=10_000)

38. print("%s (%s): min=%f, max=%f, avg=%f" % (kk, k, min(t), max(t), sum(t)/len(t)))

39. # for

40. # for

Et (au bout d’environ 12 minutes) un résultat…

1. start=10000, repeat=20

2. map (with_lambda): min=8.688044, max=8.770688, avg=8.706189

3. comprehension (with_lambda): min=6.990289, max=7.134122, avg=7.006727

4. map (with function): min=8.030385, max=8.568984, avg=8.104334

5. comprehension (with function): min=11.132535, max=11.381556, avg=11.190379

XXVII-8-d - Conclusion

Il semble que l’utilisation de la fonction map() sera plus rapide d’environ 40 % face à la liste en compréhension quand le traitement se fait via une fonction déjà existante ; en opposition au traitement fait par une lambda dans lequel là la liste en compréhension sera plus rapide d’environ 70 %. Peut‑être (juste une hypothèse) parce que dans la fonction map(), la lambda est redéfinie à chaque itération…

Cependant, quand le traitement s’intensifie pour devoir opérer sur plusieurs listes, l’avantage de la liste en compréhension dans le cadre de la lambda diminue assez drastiquement (il passe à environ 30 % pour deux listes et à environ 20 % pour trois listes) tandis que l’avantage du map() utilisant une fonction déjà existante diminue moins (il passe à environ 30 % pour deux listes et reste à cette valeur pour trois listes). Probablement cette perte drastique de performances dans l’écriture en compréhension est‑elle due à l’introduction de la fonction zip() nécessaire à la jointure des listes.

Toutefois, n’oublions pas aussi l’avantage de la fonction map() en cas de listes de taille inégale. Dans ce cas elle s’adapte automatiquement à la liste la plus longue en complétant les listes plus courtes par autant de None que nécessaire. L’adaptation de ce fonctionnement dans une écriture en compréhension sera plus ardue et nuira certainement à la lisibilité de l’ensemble sans même parler des baisses de performances que cela pourra entraîner.

Remerciements

- Remerciements à Sam et Max (https://sametmax2.com) dont les explications détaillées (avec parfois un brin d’humour) sur certains concepts m’ont aidé à les comprendre pour ensuite pouvoir les exposer ici.

- Remerciements au site officiel Python (https://www.python.org) qui contient une documentation explicite sur tous les outils de son langage.

- Remerciements au forum du site Développez.com (https://www.developpez.net) et à ses nombreux contributeurs auprès de qui j’ai beaucoup appris.

Index

- @ -

class (indexable) (1)

instruction break (1)

opérateur puissance (1)

@classmethod (1)

class (itérable) (1), (2)

instruction continue (1)

opérateur soustraction (1)

@property (1)

class (mesurable) (1)

instruction def (1)

opérateur supérieur à (1)

@staticmethod (1)

class (méthodes privées) (1)

instruction do…while (1)

opérateur supérieur ou égal à (1)

- _ -

class (méthodes statiques) (1)

instruction elif (1)

operator (module) (1)

__abs__() (1)

class (méthodes) (1)

instruction else (1), (2), (3)

or (opérateur) (1)

__add__() (1)

class (property()) (1)

instruction except (1)

ord() (1)

__and__() (1)

class (staticmethod()) (1)

instruction finally (1)

os (module) (1)

__bool__() (1)

class (super()) (1)

instruction for (1)

ou (opérateur) (1)

__bytes__() (1)

class (surcharge) (1)

instruction global (1)

ou exclusif (bits à bits) (opérateur) (1)

__call__() (1)

classmethod() (1)

instruction if (1)

ou inclusif (bits à bits) (opérateur) (1)

__cmp__() (1)

clear (dict) (1)

instruction import (1)

- P -

__code__ (fonction) (1)

clear (list) (1)

instruction lambda (1)

pandas (module) (1)

__code__.co_argcount (fonction) (1)

clear (set) (1)

instruction match (1)

partition (str) (1)

__code__.co_kwonlyargcount (fonction) (1)

close() (1), (2)

instruction nonlocal (1)

pass (instruction) (1)

__code__.co_varnames (fonction) (1)

cmp() (1)

instruction pass (1)

pathlib (module) (1)

__contains__() (1)

collections (module) (1)

instruction print (1)

pickle (module) (1)

__defaults__ (fonction) (1)

commentaire (1)

instruction raise (1)

pointvirgule (instructions) (1)

__del__() (1)

configparser (module) (1)

instruction return (1)

ponctuation (caractéristiques) (1)

__delattr__ (1)

ConfigParser (module) (1)

instruction switch…case (1)

pop (dict) (1)

__dict__ (1)

continue (instruction) (1)

instruction try (1)

pop (list) (1)

__dir__() (1)

copy (dict) (1)

instruction while (1)

pop (set) (1)

__div__() (1)

copy (list) (1)

instruction with (1)

popitem (dict) (1)

__doc__ (fonction) (1)

copy (module) (1)

instruction yield (1)

print (fonction) (1)

__enter__() (1)

copy (set) (1)

instruction yield from (1)

print (instruction) (1)

__eq__() (1)

count (list) (1)

instructions (pointvirgule) (1)

property() (1)

__exit__() (1)

count (str) (1)

int (type) (1)

psutil (module) (1)

__float__() (1)

count (tuple) (1)

intéractif (mode) (1)

puissance (opérateur) (1)

__floordiv__() (1)

cpickle (module) (1)

interpréteur (1)

pwd (module) (1)

__future__ (module) (1)

Crypto (module) (1)

io (module) (1)

pygtk (module) (1)

__ge__() (1)

Cryptodome (module) (1)

is (opérateur) (1)

PyQt (module) (1)

__getattr__ (1)

csv (module) (1)

isalnum (str) (1)

PySide (module) (1)

__getattribute__ (1)

ctypes (module) (1)

isalpha (str) (1)

python 2 (1)

__getitem__() (1)

curses (module) (1)

isascii (str) (1)

python 3 (1)

__gt__() (1)

- D -

isdecimal (str) (1)

- R -

__hash__() (1)

dataclasses (module) (1)

isdigit (str) (1)

raise (1)

__iadd__() (1)

datetime (module) (1)

isidentifier (str) (1)

random (module) (1)

__iand__() (1)

décalage de bits (opérateur) (1)

isinstance() (1)

range() (1)

__idiv__() (1)

decimal (module) (1)

islower (str) (1)

raw_input() (1)

__ifloordiv__() (1)

def (1)

isnumeric (str) (1)

re (module) (1)

__ilshift__() (1)

del() (1), (2)

isprintable (str) (1)

read() (1)

__imod__() (1)

dict (clear) (1)

isspace (str) (1)

readline() (1)

__imul__() (1)

dict (copy) (1)

issubclass() (1)

readlines() (1)

__init__() (1)

dict (del()) (1)

istitle (str) (1)

reduce (iterable) (1)

__init_subclass__ (1)

dict (fonction()) (1)

isupper (str) (1)

remove (list) (1)

__int__() (1)

dict (in) (1)

items (dict) (1)

remove (set) (1)

__inv__() (1)

dict (items()) (1)

items() (1)

replace (str) (1)

__ior__() (1)

dict (items) (1)

iter() (1), (2)

return (1), (2)

__ipow__() (1)

dict (iteritems()) (1)

itérable (1)

reverse (list) (1)

__irshift__() (1)

dict (iteritems) (1)

iterable (all) (1)

reversed() (1)

__isub__() (1)

dict (iterkeys()) (1)

iterable (any) (1)

rfind (str) (1)

__iter__() (1)

dict (iterkeys) (1)

iterable (filter) (1)

rindex (str) (1)

__itruediv__() (1)

dict (itervalues()) (1)

iterable (map) (1)

rjust (str) (1)

__ixor__() (1)

dict (itervalues) (1)

iterable (max) (1)

round() (1)

__kwdefaults__ (fonction) (1)

dict (keys()) (1)

iterable (min) (1)

rpartition (str) (1)

__le__() (1)

dict (keys) (1)

iterable (reduce) (1)

rstrip (str) (1)

__len__() (1)

dict (len()) (1)

iterable (sorted) (1)

- S -

__long__() (1)

dict (manipulations) (1)

iterable (sum) (1)

scope (1)

__lshift__() (1)

dict (méthodes spécifiques) (1)

iterable (zip) (1)

script (mode) (1)

__lt__() (1)

dict (not in) (1)

iteritems (dict) (1)

seek() (1)

__mod__() (1)

dict (pop) (1)

iteritems() (1)

send() (1)

__module__ (fonction) (1)

dict (popitem) (1)

iterkeys (dict) (1)

set (add()) (1)

__mul__() (1)

dict (setdefault) (1)

iterkeys() (1)

set (add) (1)

__name__ (fonction) (1)

dict (type) (1), (2)

itertools (module) (1)

set (clear) (1)

__ne__() (1)

dict (update) (1), (2), (3)

itervalues (dict) (1)

set (copy) (1)

__neg__() (1)

dict (values()) (1)

itervalues() (1)

set (discard) (1)

__new__ (1)

dict (values) (1)

- J -

set (fonction()) (1)

__next__() (1)

dict (viewitems()) (1)

join (str) (1)

set (in) (1)

__nonzero__() (1)

dict (viewitems) (1)

json (module) (1)

set (len()) (1)

__or__() (1)

dict (viewkeys()) (1)

- K -

set (manipulations) (1)

__pos__() (1)

dict (viewkeys) (1)

keys (dict) (1)

set (méthodes spécifiques) (1)

__pow__() (1)

dict (viewvalues()) (1)

keys() (1)

set (not in) (1)

__qualname__ (fonction) (1)

dict (viewvalues) (1)

keyword (module) (1)

set (pop) (1)

__radd__() (1)

dict fromkeys) (1)

- L -

set (remove) (1)

__rand__() (1)

différent de (opérateur) (1)

lambda (1)

set (type) (1), (2)

__rdiv__() (1)

dir() (1)

len() (1), (2), (3), (4)

set (update()) (1)

__repr__() (1)

discard (set) (1)

list (append()) (1)

set (update) (1)

__rfloordiv__() (1)

division (opérateur) (1)

list (append) (1)

setdefault (dict) (1)

__rmod__() (1)

division euclidienne (opérateur) (1)

list (clear) (1)

sort (list) (1)

__rmul__() (1)

divmod() (1)

list (copy) (1)

sorted (iterable) (1)

__ror__() (1)

do…while (instruction) (1)

list (count) (1)

sorted() (1)

__rpow__() (1)

doctest (module) (1)

list (del()) (1)

soustraction (opérateur) (1)

__rshift__() (1)

- E -

list (extend()) (1)

split (str) (1)

__rsub__() (1)

égal à (opérateur) (1)

list (extend) (1)

splitlines (str) (1)

__rtruediv__() (1)

elif (instruction) (1)

list (fonction()) (1)

spwd (module) (1)

__rxor__() (1)

else (1)

list (index) (1)

startswith (str) (1)

__setattr__ (1)

else (instruction) (1), (2)

list (indices) (1)

staticmethod() (1)

__setitem__() (1)

encode (str) (1)

list (insert()) (1)

statistics (module) (1)

__slots__ (1)

endswith (str) (1)

list (insert) (1)

str (capitalize) (1)

__str__() (1)

enum (module) (1)

list (manipulations) (1)

str (casefold) (1)

__sub__() (1)

enumerate() (1)

list (méthodes spécifiques) (1)

str (center) (1)

__truediv__() (1)

erreurs (généralités) (1)

list (pop) (1)

str (count) (1)

__unicode__() (1)

et (bits à bits) (opérateur) (1)

list (remove) (1)

str (encode) (1)

__xor__() (1)

et (opérateur) (1)

list (reverse) (1)

str (endswith) (1)

_pickle (module) (1)

eval() (1)

list (sort) (1)

str (expandtabs) (1)

- A -

except (1)

list (type) (1), (2)

str (find) (1)

abc (module) (1)

exception (1)

ljust (str) (1)

str (format) (1)

abs() (1)

exceptions (assertion) (1)

locals() (1)

str (format_map) (1)

add (set) (1)

exceptions (else) (1)

logging (module) (1)

str (in) (1)

add() (1)

exceptions (except) (1)

long (type) (1)

str (index) (1)

addition (opérateur) (1)

exceptions (finally) (1)

lower (str) (1)

str (isalnum) (1)

all (iterable) (1)

exceptions (généralités) (1)

lstrip (str) (1)

str (isalpha) (1)

and (opérateur) (1)

exceptions (pardon et permission) (1)

- M -

str (isascii) (1)

annotations (1)

exceptions (raise) (1)

maketrans (str) (1)

str (isdecimal) (1)

any (iterable) (1)

exceptions (try) (1)

map (iterable) (1)

str (isdigit) (1)

append (list) (1)

exec() (1)

match (instruction) (1)

str (isidentifier) (1)

append() (1)

expandtabs (str) (1)

math (module) (1)

str (islower) (1)

assert (1)

exposant (opérateur) (1)

matplotlib (module) (1)

str (isnumeric) (1)

atexit (module) (1)

expression (1)

max (iterable) (1)

str (isprintable) (1)

- B -

extend (list) (1)

méthode __del__() (1)

str (isspace) (1)

base64 (1)

extend() (1)

méthode __init__() (1)

str (istitle) (1)

bin() (1)

- F -

méthode close() (1)

str (isupper) (1)

binascii (module) (1)

f-strings (1)

méthode f-strings (1)

str (join) (1)

blocs (caractéristiques) (1), (2)

False (1)

méthode format() (1)

str (len()) (1)

bool (fonction()) (1)

fileinput (module) (1)

méthode next() (1)

str (ljust) (1)

bool (type) (1)

filter (iterable) (1)

méthode send() (1)

str (lower) (1)

break (instruction) (1)

finally (1)

min (iterable) (1)

str (lstrip) (1)

builtins (module) (1)

find (str) (1)

mode (intéractif) (1)

str (maketrans) (1)

- C -

float (type) (1)

mode (script) (1)

str (manipulations) (1)

capitalize (str) (1)

flush() (1)

modules (__future__) (1)

str (méthodes spécifiques) (1)

caractéristiques (1)

fonction (__code__) (1)

modules (_pickle) (1)

str (not in) (1)

caractéristiques (blocs) (1), (2)

fonction (__code__.co_argcount) (1)

modules (abc) (1)

str (partition) (1)

caractéristiques (ponctuation) (1)

fonction (__code__.co_kwonlyargcount) (1)

modules (atexit) (1)

str (replace) (1)

caractéristiques (tabulations) (1)

fonction (__code__.co_varnames) (1)

modules (base64) (1)

str (rfind) (1)

casefold (str) (1)

fonction (__defaults__) (1)

modules (binascii) (1)

str (rindex) (1)

cast (1)

fonction (__doc__) (1)

modules (builtins) (1)

str (rjust) (1)

center (str) (1)

fonction (__kwdefaults__) (1)

modules (collections) (1)

str (rpartition) (1)

chaîne (base) (1)

fonction (__module__) (1)

modules (configparser) (1)

str (rstrip) (1)

chaîne (encodage) (1)

fonction (__name__) (1)

modules (ConfigParser) (1)

str (slice) (1)

chaîne (préfixe b) (1)

fonction (__qualname__) (1)

modules (copy) (1)

str (split) (1)

chaîne (préfixe r) (1)

fonction (annotations) (1)

modules (cpickle) (1)

str (splitlines) (1)

chaîne (préfixe u) (1)

fonction (dict vers paramètres) (1)

modules (Crypto) (1)

str (startswith) (1)

chaînes (formatage) (1)

fonction (généralités) (1)

modules (Cryptodome) (1)

str (strip) (1)

chaînes (indices) (1)

fonction (inclusion) (1)

modules (csv) (1)

str (swapcase) (1)

chr() (1)

fonction (introduction) (1)

modules (ctypes) (1)

str (title) (1)

class (@classmethod) (1)

fonction (lambda) (1)

modules (curses) (1)

str (translate) (1)

class (@property) (1)

fonction (list vers paramètres) (1)

modules (dataclasses) (1)

str (type) (1), (2)

class (@staticmethod) (1)

fonction (méthodes de passages verrouillées) (1)

modules (datetime) (1)

str (upper) (1)

class (__abs__) (1)

fonction (paramètres facultatifs) (1)

modules (decimal) (1)

StringIO (module) (1)

class (__add__) (1)

fonction (paramètres nommés) (1)

modules (doctest) (1)

strip (str) (1)

class (__and__) (1)

fonction (paramètres par défaut) (1)

modules (enum) (1)

struct (module) (1)

class (__bool__) (1), (2)

fonction (paramètres positionnels) (1)

modules (fileinput) (1)

subprocess (module) (1)

class (__bytes__) (1)

fonction (paramètres) (1)

modules (fraction) (1)

sum (iterable) (1)

class (__call__()) (1)

fonction (récursivité) (1), (2)

modules (functools) (1)

super() (1)

class (__cmp__) (1)

fonction (retour) (1)

modules (généralité) (1)

supérieur à (opérateur) (1)

class (__contains__()) (1)

fonction (syntaxe) (1)

modules (getopt) (1)

supérieur ou égal à (opérateur) (1)

class (__del__) (1)

fonction (tuple vers paramètres) (1)

modules (grp) (1)

swapcase (str) (1)

class (__delattr__) (1)

fonction abs() (1)

modules (hashlib) (1)

switch…case (instruction) (1)

class (__dict__) (1)

fonction anonyme (1)

modules (import) (1)

sys (module) (1)

class (__dir__()) (1)

fonction bin() (1)

modules (importlib) (1)

- T -

class (__div__) (1)

fonction chr() (1)

modules (inspect) (1)

tabulations (caractéristiques) (1)

class (__enter__()) (1)

fonction close() (1)

modules (io) (1)

tell() (1)

class (__eq__) (1)

fonction cmp() (1)

modules (itertools) (1)

time (module) (1)

class (__exit__()) (1)

fonction dir() (1)

modules (json) (1)

timeit (module) (1)

class (__float__) (1)

fonction divmod() (1)

modules (keyword) (1)

title (str) (1)

class (__floordiv__) (1)

fonction enumerate() (1)

modules (logging) (1)

tkinter (module) (1)

class (__ge__) (1)

fonction eval() (1)

modules (math) (1)

Tkinter (module) (1)

class (__getattr__) (1)

fonction exec() (1)

modules (matplotlib) (1)

translate (str) (1)

class (__getattribute__) (1)

fonction flush() (1)

modules (numbers) (1)

True (1)

class (__getitem__()) (1)

fonction globals() (1)

modules (numpy) (1)

truncate() (1)

class (__gt__) (1)

fonction hash() (1)

modules (operator) (1)

try (1)

class (__hash__()) (1)

fonction help() (1)

modules (os) (1)

tuple (count) (1)

class (__iadd__) (1)

fonction hex() (1)

modules (pandas) (1)

tuple (fonction()) (1)

class (__iand__) (1)

fonction id() (1)

modules (pathlib) (1)

tuple (in) (1)

class (__idiv__) (1)

fonction input() (1), (2)

modules (pdb) (1)

tuple (index) (1)

class (__ifloordiv__) (1)

fonction isinstance() (1)

modules (pickle) (1)

tuple (indices) (1)

class (__ilshift__) (1)

fonction issubclass() (1)

modules (psutil) (1)

tuple (len()) (1)

class (__imod__) (1)

fonction iter() (1), (2)

modules (pwd) (1)

tuple (manipulations) (1)

class (__imul__) (1)

fonction locals() (1)

modules (pygtk) (1)

tuple (méthodes spécifiques) (1)

class (__init__()) (1)

fonction next() (1), (2)

modules (PyQt) (1)

tuple (not in) (1)

class (__init_subclass__) (1)

fonction oct() (1)

modules (PySide) (1)

tuple (parenthèses) (1)

class (__int__) (1)

fonction open() (1)

modules (random) (1)

tuple (type) (1), (2)

class (__inv__) (1)

fonction ord() (1)

modules (re) (1)

type (dict) (1)

class (__ior__) (1)

fonction print (1)

modules (spwd) (1)

type (float) (1)

class (__ipow__) (1)

fonction range() (1)

modules (statistics) (1)

type (int) (1)

class (__irshift__) (1)

fonction raw_input() (1)

modules (StringIO) (1)

type (list) (1)

class (__isub__) (1)

fonction read() (1)

modules (struct) (1)

type (long) (1)

class (__iter__()) (1)

fonction readline() (1)

modules (subprocess) (1)

type (set) (1)

class (__itruediv__) (1)

fonction readlines() (1)

modules (sys) (1)

type (str) (1)

class (__ixor__) (1)

fonction reversed() (1)

modules (time) (1)

type (tuple) (1)

class (__le__) (1)

fonction round() (1)

modules (timeit) (1)

type bool (1)

class (__len__()) (1)

fonction seek() (1)

modules (tkinter) (1)

type dict (1)

class (__long__) (1)

fonction sorted() (1)

modules (Tkinter) (1)

type list (1)

class (__lshift__) (1)

fonction super() (1)

modules (types) (1)

type None (1)

class (__lt__) (1)

fonction tell() (1)

modules (typing) (1)

type set (1)

class (__mod__) (1)

fonction truncate() (1)

modules (unicodedata) (1)

type str (1)

class (__mul__) (1)

fonction type() (1), (2)

modules (unittest) (1)

type str (opérateurs) (1)

class (__ne__) (1)

fonction write() (1)

modules (uuid) (1)

type tuple (1)

class (__neg__) (1)

fonction writelines() (1)

modules (xml) (1)

type() (1)

class (__new__) (1)

fonction xrange() (1)

modules (zlib) (1)

type() (fonction) (1)

class (__next__()) (1)

for (instruction) (1)

modulo (opérateur) (1)

types (module) (1)

class (__nonzero__) (1)

format (str) (1)

morse (opérateur) (1)

typing (module) (1)

class (__or__) (1)

format() (1)

multiplication (opérateur) (1)

- U -

class (__pos__) (1)

format_map (str) (1)

- N -

unicodedata (module) (1)

class (__pow__) (1)

fraction (module) (1)

next() (1), (2), (3)

unittest (module) (1)

class (__radd__) (1)

fromkeys (dict) (1)

non (bits à bits) (opérateur) (1)

update (dict) (1), (2), (3)

class (__rand__) (1)

functools (module) (1)

None (1)

update (set) (1)

class (__rdiv__) (1)

- G -

None (type) (1)

update() (1)

class (__repr__) (1)

générateurs (close) (1)

nonlocal (1)

upper (str) (1)

class (__rfloordiv__) (1)

générateurs (généralités) (1)

not (opérateur) (1)

uuid (module) (1)

class (__rmod__) (1)

générateurs (next) (1), (2)

not in (1), (2), (3), (4)

- V -

class (__rmul__) (1)

générateurs (return) (1)

numbers (module) (1)

values (dict) (1)

class (__ror__) (1)

générateurs (send) (1)

numpy (module) (1)

values() (1)

class (__rpow__) (1)

générateurs (yield from) (1)

- O -

variables (1)

class (__rshift__) (1)

générateurs (yield) (1)

oct() (1)

viewitems (dict) (1)

class (__rsub__) (1)

getopt (module) (1)

open() (1)

viewitems() (1)

class (__rtruediv__) (1)

global (1)

opérateur addition (1)

viewkeys (dict) (1)

class (__rxor__) (1)

globals() (1)

opérateur and (1)

viewkeys() (1)

class (__setattr__) (1)

grp (module) (1)

opérateur de décalage de bits (1)

viewvalues (dict) (1)

class (__setitem__()) (1)

- H -

opérateur différent de (1)

viewvalues() (1)

class (__slots__) (1)

hash() (1)

opérateur division (1)

- W -

class (__str__) (1)

hashlib (module) (1)

opérateur division euclidienne (1)

while (instruction) (1)

class (__sub__) (1)

help() (1)

opérateur égal à (1)

with (1)

class (__truediv__) (1)

hex() (1)

opérateur et (1)

write() (1)

class (__unicode__) (1)

- I -

opérateur et (bits à bits) (1)

writelines() (1)

class (__xor__) (1)

id() (1)

opérateur exposant (1)

- X -

class (attributs privés) (1)

if (instruction) (1)

opérateur in (1), (2), (3), (4)

xml (module) (1)

class (attributs publics) (1)

import (1)

opérateur inférieur à (1)

xrange() (1)

class (attributs statiques) (1)

importlib (module) (1)

opérateur inférieur ou égal à (1)

- Y -

class (attributs) (1)

in (1), (2), (3), (4)

opérateur is (1)

yield (1)

class (callable) (1)

index (list) (1)

opérateur modulo (1)

yield from (1)

class (classmethod()) (1)

index (str) (1)

opérateur morse (1)

- Z -

class (constructeur) (1), (2)

index (tuple) (1)

opérateur multiplication (1)

zip (iterable) (1)

class (contenance) (1)

inférieur à (opérateur) (1)

opérateur non (bits à bits) (1)

zlib (module) (1)

class (context manager) (1), (2)

inférieur ou égal à (opérateur) (1)

opérateur not (1)

class (destructeur) (1), (2)

input() (1)

opérateur not in (1), (2), (3), (4)

class (dir) (1)

insert (list) (1)

opérateur or (1)

class (généralités) (1)

insert() (1)

opérateur ou (1)

class (hashable) (1)

inspect (module) (1)

opérateur ou exclusif (bits à bits) (1)

class (héritage) (1)

instruction assert (1)

opérateur ou inclusif (bits à bits) (1)

(1)Une anthologie de sketches, de parodies d'émissions télévisées et de reportages improbables qui dénoncent les travers de la société britannique.

(2)Certaines traductions remplacent « néerlandais » par « allemand ». Quant à la raison de cette remarque… ?