Si vous avez déjà codé en Python, il est fort probable que vous ayez utilisé le mot-clé with
de cette manière :
with open("sfeir.txt") as file:
data = file.read()
print(data)
Cependant, le fonctionnement précis de l'instruction with
reste souvent méconnu. En jetant un regard en coulisses et en explorant les gestionnaires de contexte sous-jacents, nous allons découvrir les mécanismes qui donnent vie à cette instruction.
Contexte ?
L'instruction with
offre une méthode simple et puissante pour gérer les ressources à l'aide de gestionnaires de contexte. Cette approche suit le patron de conception classique try... except... finally
et permet d'encapsuler l'exécution d'un bloc de code de manière propre et sécurisée.
Un gestionnaire de contexte (ou context manager) gère donc l'acquisition et la libération de ressources dans un bloc de code. Il a pour but de garantir que les ressources sont correctement initialisées et nettoyées, même en cas d'erreur ou d'exception.
Un context manager doit implémenter deux méthodes spéciales : __enter__()
et __exit__()
. La méthode __enter__()
est appelée au début du bloc with
et est responsable de l'initialisation des ressources. Elle renvoie généralement l'objet qui sera associé au gestionnaire de contexte. La méthode __exit__()
est appelée à la fin du bloc with
et est responsable de la libération des ressources.
Avec ce premier gestionnaire basique, on voit clairement l'ordre d'exécution.
class MyContextManager:
def __enter__(self):
print("Initialisation des ressources")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Nettoyage des ressources")
with MyContextManager() as cm:
print("Exécution du bloc de code")
$ python contexte.py
Initialisation des ressources
Exécution du bloc de code
Nettoyage des ressources
Ainsi, on peut s'amuser à re-coder un context manager qui va lire ou écrire dans des fichiers. On utilse la fonction open()
qui nous renvoie un flux de données[1]. Le flux est ouvert dans la méthode __enter__()
et on n'oublie pas de le fermer dans la fonction __exit__()
.
class Fichier:
def __init__(self, nom, mode):
self.nom = nom
self.mode = mode
def __enter__(self):
self.fichier = open(self.nom, self.mode)
return self.fichier
def __exit__(self, exc_type, exc_value, traceback):
self.fichier.close()
with Fichier("sfeir.txt", 'w') as f:
f.write("sfeir.dev")
La classe, non ?
Python fournit également le module contextlib
qui propose un décorateur @contextmanager
afin de créer des gestionnaires de contexte de manière plus concise, sans avoir à définir une classe complète. Si on remplace notre classe Fichier
, ça donne ceci :
from contextlib import contextmanager
@contextmanager
def fichier(nom, mode):
try:
f = open(nom, mode)
yield f
finally:
f.close()
with fichier("sfeir.txt", 'w') as f:
f.write("sfeir.dev")
Avec le décorateur, on transforme la fonction fichier()
en un gestionnaire de contexte. La fonction utilise le mot-clé yield
pour indiquer le point d'entrée et de sortie du contexte. Autrement dit, on prépare les ressources nécessaires pour le contexte avant yield
puis on les détruit après.
Dans notre code, on utilise yield
pour retourner le flux qui sera utilisé à l'intérieur du bloc with
. Une fois le bloc terminé, la partie finally
est exécutée pour fermer le fichier.
Les context managers en action
On retrouve les gestionnaires de contexte aussi bien dans la librairie standard de Python que dans les modules populaires. Voici quelques exemples supplémentaires de gestionnaires de contexte dans différents domaines.
# Gestion d'une connexion vers une base données
import sqlite3
with sqlite3.connect("database.db") as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM table")
results = cursor.fetchall()
print(results)
# Gestion d'une session dans le module requests
# Une session conserve les paramètres et les cookies entre plusieurs requêtes
import requests
with requests.Session() as session:
# Effectuer des requêtes HTTP ici
response = session.get("https://www.sfeir.dev")
print(response.status_code)
# Gestion d'une connexion vers un Redis
import redis
with redis.Redis(host='localhost', port=6379) as r:
r.set('cle', 'valeur')
valeur = r.get('cle')
print(valeur)
# Gestion de threads concurrents
# Ici, 10 threads doivent incrémenter la même variable
# Sans le lock, les threads peuvent accéder simultanément à la variable partagée,
# ce qui peut entraîner des incohérences et des erreurs de valeur.
# Le lock permet d'assurer qu'un seul thread à la fois peut modifier la variable,
# garantissant ainsi la cohérence des données.
import threading
lock = threading.Lock()
counter = 0
def thread_function():
global counter
thread_name = threading.current_thread().name
for _ in range(10):
with lock:
counter += 1
print(f"{thread_name} incrémente, valeur du compteur : {counter}")
threads = [threading.Thread(target=thread_function, name=f"Thread {i}") for i in range(1, 11)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
Niveau supérieur
Pour aller plus loin, les gestionnaires peuvent se souvenir de l'état précédent et adapter leur comportement en fonction du contexte. Cela permet de créer des contextes imbriqués où certaines actions sont effectuées à l'entrée et à la sortie de chaque niveau de contexte. On parle alors de gestionnaire réentrant. Voici un exemple de ce type de context manager :
class Indentation:
def __init__(self):
self.niveau = 0
def __enter__(self):
self.niveau += 1
return self
def __exit__(self, exc_type, exc_val, traceback):
self.niveau -= 1
def imprimer(self, texte):
print(f"{' ' * self.niveau}{texte}")
with Indentation() as indent:
indent.imprimer('Niveau 1')
with indent:
indent.imprimer('Niveau 2')
with indent:
indent.imprimer('Niveau 3')
indent.imprimer('Encore niveau 1')
$ python contexte.py
Niveau 1
Niveau 2
Niveau 3
Encore niveau 1
Dans cet exemple, Indentation
maintient un état niveau
qui est incrémenté à chaque niveau de contexte imbriqué et décrémenté lors de la sortie de chaque niveau. Cela permet d'adapter le comportement du manager réentrant en fonction du niveau de contexte actuel. En sortie, on voit la structure qui reflète la profondeur du contexte grâce à l'indentation.
Pour les curieux
Je vous renvoie vers la documentation officielle du module contextlib
. Cette page revient sur le décorateur que l'on a rapidement évoqué mais parle aussi des gestionnaires de contexte asynchrones, des gestionnaires réutilisables comme le Lock
ou encore du gestionnaire ExitStack
.
Si vous avez bien suivi,
open
retourne un objet qui est un flux, mais qui est aussi un context manager qu'on peut utiliser avecwith
. Pour en savoir plus, voici les liens vers la documentation de la fonction open et de l'objet IOBase. ↩︎