Gestionnaires de contexte en Python
par Vincent Poulailleau - 12 minutes de lecture - 2456 mots
Sommaire :
Pourquoi cette présentation ?
Le but des gestionnaires de contexte est d’écrire moins de code, de faire moins de tests, de faciliter la maintenance et l’évolutivité. Ils s’appliquent à un type d’actions récurrentes décrites ci-dessous.
Vous trouverez un exercice corrigé à la fin de cette présentation.
Théorie
Pourquoi les gestionnaires de contexte ?
Les context managers ou gestionnaires de contexte sont apparus dans Python 2.5 avec la PEP 343 il y a une quinzaine d’années. C’est une structure de code importante à connaître dès que vous faites des projets conséquents.
Nous avons, en programmant, régulièrement des séries d’actions du type :
- phase initiale générique
- phase intermédiaire différente à chaque fois
- phase finale générique
Le but des context managers est de n’écrire qu’une seule fois le code de la phase initiale générique et de la phase finale générique. Moyennant une syntaxe particulière, nous pourrons ré-utiliser ces phases génériques autour de notre code intermédiaire spécifique.
Que pouvons-nous trouver comme exemple de phases génériques ?
- ouvrir / fermer (pour un fichier, une socket, etc.)
- verrouiller / déverrouiller (pour une ressource critique, pour un
threading.Lock
, etc.) - modifier / réinitialiser (pour une transaction de base de données relationnelle, pour des ressources temporaires en particulier lors de l’écriture de tests, etc.)
- entrer / sortir (dans un dossier, dans une balise HTML, etc.)
- démarrer / arrêter (un chronomètre, une tâche de fond, etc.)
Raymond Hettinger évoque la notion de sandwich : nous voulons toujours les mêmes tranches de pain en dessous et au-dessus, mais nous voulons pouvoir changer le contenu à l’intérieur.
Utiliser un gestionnaire de contexte
Vous avez certainement déjà utilisé un gestionnaire de contexte.
Certains sites internet vous disent que pour utiliser un fichier, il faut faire un code du style :
|
|
C’est une mauvaise idée pour deux raisons.
La première ne nous concerne pas pour cet article, mais sachez que l’encodage utilisé pour ce fichier texte ne sera pas le même selon le système d’exploitation que vous utilisez.
Pour résoudre ce problème, il faut plutôt utiliser :
|
|
Une autre technique est d’attendre Python 3.10 et sa PEP 597, qui définit UTF-8 comme encodage par défaut.
La deuxième raison qui nous pousse à améliorer le code précédent est que si les autres opérations (do_some_operations()
et do_other_operations()
) plantent (lèvent une exception pour être exact), alors my_file.close()
n’est jamais atteint et n’est donc pas exécuté. Le fichier ne sera alors pas fermé comme il se doit (dans la pratique, Python essaie de le fermer à un moment « judicieux »).
Vous pourriez donc écrire :
|
|
C’est une solution techniquement correcte, mais verbeuse. Elle va un peu à l’encontre de la PEP 20 qui nous dit que : « la lisibilité, ça compte ».
La solution est d’utiliser un context manager pour gérer l’ouverture et la fermeture du fichier. Pour activer ce gestionnaire de contexte, nous utilisons le mot clé with
.
|
|
my_file.close()
sera appelé en automatique par le gestionnaire de contexte, à la sortie du bloc with
.
Autre exemple classique, vous pouvez, à partir d’un moment dans votre code, vouloir ignorer certaines exceptions, et plus tard ne plus les ignorer.
Ceci peut s’écrire :
|
|
Et avec un context manager, cela donne :
|
|
Un dernier exemple pour finir, imaginez que vous avez une fonction qui passe son temps à appeler print
. Vous pouvez vouloir ne pas afficher les résultats des print
ou encore les rediriger vers un fichier. Vous pouvez le faire avec un gestionnaire de contexte.
|
|
Vous avez pu noter que, dans certains cas, nous pouvons accéder au contexte créé par le gestionnaire de contexte. Cela se fait avec le mot clé as
. Dans l’exemple précédent, le fichier ouvert est stocké dans une variable appelée log
. Nous aurions donc pu faire un log.write("Joli titre du document\n")
.
Créer un gestionnaire de contexte
Un gestionnaire basique
Nous avons besoin, pour créer un gestionnaire de contexte, de deux fonctions :
- une appelée au début
- une appelée à la fin
- éventuellement la possibilité de donner accès au contexte créé
Pour grouper ces fonctions, nous allons créer une classe. Afin que Python puisse s’y repérer, le nom des fonctions appelées au début et à la fin est standardisé dans la PEP 343 et documenté ici :
- https://docs.python.org/fr/3/reference/datamodel.html#with-statement-context-managers
- https://docs.python.org/fr/3/library/stdtypes.html#typecontextmanager
Crééons un exemple de gestionnaire de contexte pas très utile, mais montrant les bases :
|
|
À l’exécution, nous aurons :
|
|
Notez que dans le dernier exemple, le code 1 / 0
a planté (levé une exception ZeroDivisionError
). Cela n’a pas empeché le gestionnaire de contexte d’afficher la phrase de conclusion. Par contre la fin du contenu du bloc with
n’est pas exécutée.
Python a considéré que nous avions correctement géré l’exception car la méthode __exit__
de notre DummyCM
a renvoyé True
.
Remplaçons le return True
par un return False
:
|
|
Cela affiche au final :
|
|
Vous pouvez noter que, dans le dernier exemple, la phrase de conclusion (venant de __exit__
) est bien affichée. Mais l’exception ZeroDivisionError
n’est plus gérée par le gestionnaire de contexte, ce sera à l’utilisateur de s’en préoccuper.
Gestion générique des exceptions
Admettons que vous souhaitiez gérer un certain type d’exception de manière générique. Nous utiliserons dans ce cas, les trois arguments fournis à __exit__
. Si une exception est survenue lors de l’exécution du corps de l’instruction with
, les arguments de __exit__
contiennent le type de l’exception, sa valeur, et la trace de la pile (traceback). Sinon les trois arguments valent None
.
|
|
Ce qui affiche à l’exécution :
|
|
Nous voyons dans le premier exemple que le gestionnaire de contexte gère proprement la division par 0. Le reste du bloc with
est ignoré, mais l’exception est attrapée proprement.
Le deuxième exemple est similaire, la seconde exception n’est pas levée car l’exécution du bloc with
est arrêtée à la gestion de la première exception.
Dans le troisième exemple, l’accès à un mauvais élément de la liste a
lève une exception de type IndexError
. Celle-ci n’est pas gérée par le gestionnaire de contexte, car dans ce cas __exit__
renvoie False
. Le reste du bloc with
est ignoré, mais l’exception n’est pas attrapée proprement. C’est du ressort de l’utilisateur de s’en occuper.
Récupération du contexte
Avec le mot clé as
, nous pouvons récuppérer le contexte créé par le gestionnaire de contexte, comme nous l’avions fait dans l’exemple suivant :
|
|
Ce contexte est tout simplement la valeur renvoyée par __enter__
.
|
|
À l’éxécution, cela affichera :
|
|
Et avec un générateur ?
Vous êtes plus à l’aise avec les fonctions qu’avec les classes ? Vous savez ce qu’est un générateur ?
Je ne vais pas m’étendre sur le sujet, mais il est possible de convertir une fonction génératrice en gestionnaire de contexte avec contextlib.contextmanager
.
Travaux pratiques
Énoncé
En utilisant time.process_time(), réalisez un gestionnaire de contexte qui mesure le temps d’exécution du contenu du bloc with
et qui affiche ce temps avec un print
.
Pour mesurer un temps, vous pouvez écrire ce genre de code :
|
|
Ce qui donne à l’exécution :
|
|
Corrigé
|
|
Résumé
Le but des gestionnaires de contexte est d’écrire moins de code, de faire moins de tests, de faciliter la maintenance et l’évolutivité.
Nous avons, en programmant, régulièrement des séries d’actions du type :
- phase initiale générique
- phase intermédiaire différente à chaque fois
- phase finale générique
Le but des context managers est de n’écrire qu’une seule fois le code de la phase initiale générique et de la phase finale générique.
Exemples de phases génériques :
- ouvrir / fermer
- verrouiller / déverrouiller
- modifier / réinitialiser
- entrer / sortir
- démarrer / arrêter
Les gestionnaires de contexte s’utilisent avec le mot clé with
éventuellement suivi de as
pour accéder au contexte nouvellement créé.
Un gestionnaire de contexte a deux méthodes :
__enter__
appelée au début du blocwith
, peut renvoyer le contexte nouvellement créé__exit__
appelée à la fin du blocwith
, peut gérer les exceptions en fonction de la valeur de retour
Documents complémentaires
En anglais
- https://jeffknupp.com/blog/2016/03/07/python-with-context-managers/
- https://www.python.org/dev/peps/pep-0343/
- https://dbader.org/blog/python-context-managers-and-with-statement
En français
- http://sametmax.com/les-context-managers-et-le-mot-cle-with-en-python/ (attention c’est du Python 2, mais ça ressemble beaucoup au Python 3)
Conclusion
Enfin, n’hésitez pas à commenter à la fin de cette page (tout en bas). Dites si vous avez aimé ou non cet article. Signalez des informations complémentaires, des idées d’exercices. Et si un sujet ne semble pas clair, posez une question.