Les gestionnaires de contexte et l’instruction ‘with’ en Python
- 2021-08-03
- Publié par : Christophe DELEUZE
- Catégorie : Python
L’instruction with en Python est parfois considérée comme quelque chose d’obscure par certains. Mais lorsque vous jetez un œil dans les coulisses du protocole des gestionnaires de contexte (Context Manager) sous-jacent, vous verrez que tout devient plus clair.
L'instruction with en Python
L’instruction with permet de simplifier certains modèles courants de gestion des ressources en faisant abstraction de leurs fonctionnalités et en leur permettant d’être factorisés et réutilisés. En retour, cela vous aide à écrire du code plus expressif et permet d’éviter plus facilement les fuites de ressources dans vos programmes.
Un bon moyen de voir cette fonctionnalité utilisée efficacement est de regarder des exemples dans la bibliothèque standard Python. Un exemple bien connu concerne la fonction open() :
with open('hello.txt', 'w') as f:
f.write('hello, world!')
Ici, l’ouverture du fichier hello.txt se fait à l’aide de l’instruction with. L’emploi de with avec open() est couramment et généralement recommandé, car cela garantit que les fichiers ouverts seront fermés automatiquement une fois que l’exécution du programme quittera le contexte de l’instruction with. En interne, l’exemple de code ci-dessus traduit de manière simplifiée le fonctionnement de l’instruction with :
f = open('hello.txt', 'w')
try:
f.write('hello, world')
finally:
f.close()
Vous remarquerez que c’est déjà un peu plus verbeux. Notez que j’ai utilisé try et finally et que je ne me suis pas contenté de quelque chose comme ceci :
f = open('hello.txt', 'w')
f.write('hello, world')
f.close()
Cela s’explique par le fait que cette implémentation ne garantit pas que le fichier soit fermé si une exception était levée pendant l’appel f.write() et donc dans ce cas notre programme n’aurait pas libéré la ressource à l’aide de f.close() puisque cette méthode n’aurait jamais été appelée à cause de l’exception. C’est pourquoi la déclaration with est si utile. Cela facilite l’acquisition et la libération des ressources correctement.
L'instruction with avec tempfile
Un autre bon exemple où l’instruction with est utilisée efficacement est celui de la gestion de fichiers et de dossiers temporaires.
Pour cela, rien de plus simple, nous allons utiliser le module tempfile qui sur l’appel de sa méthode .close() ferme le fichier temporaire qu’il a créé et le supprime dans la foulée.
import tempfile
#--> Méthode manuelle
# creer un fichier temporaire
fp = tempfile.TemporaryFile()
try:
# Ecrire des données dedans
fp.write(b'Hello world!')
# Lire des données du fichier
fp.seek(0)
print(fp.read()) # b'Hello world!'
finally :
# fermer le fichier et le supprimer
fp.close()
#--> Méthode avec with
with tempfile.TemporaryFile() as fp:
fp.write(b'Hello world!')
fp.seek(0)
print(fp.read()) # b'Hello world!'
#-->En bonus, la création d'un dossier temporaire avec with
with tempfile.TemporaryDirectory() as tmpdirname:
print('création du dossier temporaire:', tmpdirname)
L'instruction with avec threading
Enfin, pour enfoncer le clou, un dernier bon exemple où with est utilisée efficacement dans la bibliothèque standard Python concerne son utilisation avec la classe threading.Lock :
import threading
lock = threading.Lock()
# Sans with:
lock.acquire()
try:
# Faire des trucs...
finally:
lock.release()
# Avec with:
with lock:
# Faire des trucs...
Dans les trois cas, l’utilisation d’une instruction with vous permet d’abstraire la majeure partie de la logique de gestion des ressources. Au lieu d’avoir à écrire une déclaration try et finally explicite à chaque fois, with s’en occupe pour nous.
L’instruction with peut rendre le code traitant des ressources système plus lisible. Cela permet également d’éviter les bugs ou les fuites en rendant presque impossible d’oublier le nettoyage ou la libération d’une ressource une fois que nous en avons terminé avec celle-ci.
Utiliser with dans vos propres objets
Maintenant, il n’y a rien de spécial ou de magique dans la fonction open() ou la classe threading.Lock et le fait qu’elles puissent être utilisées avec une instruction with. Vous pouvez utiliser la même fonctionnalité dans vos propres classes et fonctions en implémentant des gestionnaires de contexte.
Un gestionnaire de contexte (Context Manager) est un simple protocole (ou interface) que votre objet doit suivre pour pouvoir être utilisé avec l’instruction with. Fondamentalement, tout ce que vous avez à faire est d’ajouter des méthodes __enter__ et __exit__ à un objet si vous voulez qu’il fonctionne comme un gestionnaire de contexte. Python appellera ces deux méthodes aux moments appropriés du cycle de gestion des ressources.
Voyons à quoi cela ressemblerait concrètement. Voici à quoi open() pourrait ressembler avec une implémentation simple utilisant un Context Manager :
class ManagedFile:
def __init__(self, name):
self.name = name
def __enter__(self):
self.file = open(self.name, 'w')
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
Notre classe ManagedFile suit le protocole du gestionnaire de contexte et prend désormais en charge l’instruction with, tout comme l’exemple d’origine avec la fonction open() :
with ManagedFile('hello.txt') as f:
f.write('hello, world!')
f.write('bye now')
Python appelle __enter__ lorsque l’exécution entre dans le contexte de l’instruction with et qu’il est temps d’acquérir la ressource. Lorsque l’exécution quitte à nouveau le contexte, Python appelle __exit__ pour libérer la ressource.
Le module contextlib
L’écriture d’un gestionnaire de contexte basé sur des classes n’est pas le seul moyen de prendre en charge l’instruction with en Python. Le module contextlib de la bibliothèque standard fournit quelques abstractions supplémentaires basées sur le protocole de base du gestionnaire de contexte. Cela peut vous faciliter la vie si vos cas d’utilisation correspondent à ceux proposés par contextlib.
Par exemple, vous pouvez utiliser le décorateur @contextmanager pour définir une fonction d’usine basée sur un générateur pour une ressource qui prendra alors automatiquement en charge l’instruction with. Voici à quoi ressemble la réécriture de notre gestionnaire de contexte ManagedFile avec cette technique :
from contextlib import contextmanager
@contextmanager
def managed_file(name):
try:
f = open(name, 'w')
yield f
finally:
f.close()
with managed_file('hello.txt') as f:
f.write('hello, world!')
f.write('bye now')
D’abord, on utilise le décorateur @contextmanager pour dire à Python que la fonction managed_file() est un gestionnaire de contexte.
Ensuite, on fait un try/finally car comme nous ne sommes pas dans une classe, il n’est pas automatique comme avec __enter__ et __exit__.
yield sépare le code en deux : tout ce qui est avant est l’équivalent de __enter__, tout ce qui est après est l’équivalent de __exit__. Ce qui est yieldé est-ce que l’on récupère avec le mot clé as.
Donc dans notre cas, managed_file() est un générateur qui acquiert en premier la ressource. Ensuite, il suspend temporairement sa propre exécution et cède la ressource afin qu’elle puisse être utilisée par l’appelant. Lorsque l’appelant quitte le contexte with, le générateur continue de s’exécuter afin que toutes les étapes de nettoyage restantes puissent se produire et que la ressource puisse être libérée dans le système.
Les implémentations basées sur les classes et celles basées sur le générateur sont pratiquement équivalentes. Selon celui que vous trouvez le plus lisible, vous pourriez préférer l’un à l’autre.
Un inconvénient de l’implémentation basée sur @contextmanager peut-être qu’elle nécessite une compréhension des concepts Python avancés, tels que les décorateurs et les générateurs.
Encore une fois, faire le bon choix d’implémentation dépend de ce que vous et votre équipe êtes à l’aise d’utiliser et trouvez le plus lisible.
Écrire de jolies API avec des gestionnaires de contexte
Les gestionnaires de contexte sont assez flexibles et si vous utilisez la déclaration with de manière créative, vous pourrez définir des API pratiques pour vos modules et classes.
Par exemple, prenons le cas d’une ressource que nous voulons gérer afin de maitriser les niveaux d’indentation d’un texte dans un programme générateur de rapports. Pour le faire, si nous pouvions écrire du code comme celui-ci :
with Indenter() as indent:
indent.print('hi!')
with indent:
indent.print('hello')
with indent:
indent.print('bonjour')
indent.print('hey')
Cela se lirait presque comme un langage spécifique au domaine (DSL) pour l’indentation du texte. Notez également comment ce code entre et sort plusieurs fois du même gestionnaire de contexte pour modifier les niveaux d’indentation. L’exécution de cet extrait de code devrait conduire à la sortie suivante et imprimer du texte correctement formaté :
hi!
hello
bonjour
hey
Comment implémenteriez-vous un gestionnaire de contexte pour prendre en charge cette fonctionnalité ?
Soit dit en passant, cela pourrait être un excellent exercice pour comprendre comment fonctionnent les gestionnaires de contexte. Donc, avant de vérifier ma mise en œuvre ci-dessous, vous pouvez prendre un certain temps et essayer de la mettre en œuvre vous-même comme exercice d’apprentissage.
Prêt ? Voici comment nous pourrions implémenter cette fonctionnalité à l’aide d’un gestionnaire de contexte basé sur des classes :
class Indenter:
def __init__(self):
self.level = 0
def __enter__(self):
self.level += 1
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.level -= 1
def print(self, text):
print(' ' * self.level + text)
Enfin, un autre bon exercice serait d’essayer de re-factoriser ce code pour qu’il soit basé sur un générateur. Toutefois, je vous laisse le soin d’essayer par vous-même.
Le mot de la fin
Vous l’aurez compris, l’instruction with simplifie la gestion des exceptions en encapsulant les utilisations standard des instructions try etfinally dans ce que l’on appelle les gestionnaires de contexte (Context Manager). Le plus souvent, with est utilisé pour gérer l’acquisition et la libération en toute sécurité des ressources système. Les ressources sont acquises par l’instruction with et libérées automatiquement lorsque l’exécution quitte le contexte with. Une utilisation efficace de with peut vous aider à éviter les fuites de ressources et rendre votre code plus facile à lire.
J’espère que cet article vous aura plus, n’hésitez pas à commenter et partager ou à me poser vos questions en commentaires.
Super article!.
Un article sur les générateurs et je serai capable de bien comprendre tout les aspects ^^
Bonjour,
L’article sur les générateurs est prêt et il sort dans 2 semaines :).
Christophe