L'instruction with et les gestionnaires de contexte en Python
Apprenez comment fonctionnent l'instruction with et les gestionnaires de contexte en Python, comment créer les vôtres avec __enter__/__exit__ et contextlib.
L'instruction with garantit que les ressources telles que les fichiers, les connexions réseau et les verrous sont correctement initialisées et libérées — même lorsqu'une exception interrompt le bloc. L'objet qui contrôle cette initialisation et ce nettoyage s'appelle un gestionnaire de contexte.
Ce chapitre explique comment fonctionne l'instruction with, quand l'utiliser, comment écrire vos propres gestionnaires de contexte avec __enter__ et __exit__, et comment en créer des plus légers avec contextlib.contextmanager.
Pourquoi with existe
Avant l'instruction with, la gestion des ressources impliquait d'écrire des blocs try/finally manuellement :
f = open("data.txt", "r", encoding="utf-8")
try:
content = f.read()
finally:
f.close() # must always close, even if read() raisesCela fonctionne, mais c'est verbeux, facile à oublier, et ajoute du code répétitif autour de chaque ressource. L'instruction with condense tout cela en un seul bloc lisible et gère le nettoyage automatiquement :
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# f is closed here, no matter what happened inside the blockLa clause as f lie la valeur du gestionnaire de contexte au nom f. Certains gestionnaires de contexte ne produisent pas de valeur utile, auquel cas vous pouvez omettre as :
with some_lock:
shared_data.append(item)Comment fonctionne l'instruction with
Lorsque Python exécute une instruction with, il suit cette séquence :
- Évaluer l'expression après
with— cela produit l'objet gestionnaire de contexte. - Appeler la méthode
__enter__()du gestionnaire de contexte. La valeur de retour de__enter__()est liée à la variableas(si présente). - Exécuter le corps du bloc
with. - Appeler la méthode
__exit__(exc_type, exc_val, exc_tb)du gestionnaire de contexte.- Si le bloc s'est terminé normalement, les trois arguments valent
None. - Si une exception a été levée, les trois arguments la décrivent.
- Si
__exit__retourne une valeur vraie, l'exception est supprimée et l'exécution continue après le blocwith. Si elle retourne une valeur fausse (ouNone), l'exception se propage.
- Si le bloc s'est terminé normalement, les trois arguments valent
Ce protocole s'appelle le protocole de gestionnaire de contexte.
Ouverture de fichiers avec with
L'usage le plus courant de with est la gestion de fichiers. Les objets fichier intégrés de Python implémentent le protocole de gestionnaire de contexte, ils se ferment donc automatiquement à la fin du bloc :
with open("report.txt", "w", encoding="utf-8") as f:
f.write("Sales: 1 000\n")
f.write("Returns: 23\n")
print(f.closed) # True — file was closed on exitSi une exception se produit à l'intérieur du bloc, le fichier est quand même fermé :
try:
with open("data.txt", "r", encoding="utf-8") as f:
raise RuntimeError("something went wrong")
except RuntimeError:
pass
print(f.closed) # True — closed despite the exceptionSans with, oublier f.close() après une erreur laisse le descripteur de fichier ouvert jusqu'à ce que le ramasse-miettes s'exécute — ou jusqu'à ce que le processus se termine — ce qui peut provoquer des pertes de données ou des erreurs « too many open files » dans les programmes de longue durée.
Ouverture de plusieurs ressources à la fois
Vous pouvez ouvrir plusieurs ressources dans une seule instruction with en les séparant par des virgules (Python 3.1+) :
with open("input.txt", "r", encoding="utf-8") as src, \
open("output.txt", "w", encoding="utf-8") as dst:
for line in src:
dst.write(line.upper())C'est exactement équivalent à imbriquer deux instructions with, mais cela maintient le niveau d'indentation à plat.
Écrire un gestionnaire de contexte avec __enter__ et __exit__
Toute classe qui définit __enter__ et __exit__ peut être utilisée avec l'instruction with. Voici un exemple minimal — un minuteur qui mesure la durée d'exécution du bloc with :
import time
class Timer:
def __enter__(self):
self._start = time.perf_counter()
return self # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self._start
print(f"Elapsed: {elapsed:.4f}s")
return False # do not suppress exceptions
with Timer() as t:
total = sum(range(1_000_000))
# Elapsed: 0.0xxx s
print(total) # 499999500000Points clés :
__enter__s'exécute avant le bloc. Il retourne la valeur liée àas t. Retournerselfpermet à l'appelant d'accéder àt.elapsedet à d'autres attributs si nécessaire.__exit__s'exécute après le bloc, même en cas d'exception. RetournerFalse(ouNone) laisse toute exception se propager normalement.
Suppression des exceptions dans __exit__
Si __exit__ retourne True, l'exception est avalée et l'exécution continue après le bloc with. C'est intentionnel dans des contextes spécifiques — par exemple, un gestionnaire de contexte qui capture et journalise les erreurs sans faire planter le programme :
class Ignore:
"""Silently ignore any exception raised inside the with block."""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"Suppressed: {exc_type.__name__}: {exc_val}")
return True # suppress the exception
with Ignore():
x = 1 / 0 # ZeroDivisionError is caught and ignored
print("execution continues here")
# Suppressed: ZeroDivisionError: division by zero
# execution continues hereUtilisez la suppression des exceptions avec précaution — avaler silencieusement des erreurs peut masquer des bugs. Le contextlib.suppress de la bibliothèque standard est la manière idiomatique de faire cela (voir ci-dessous).
Un gestionnaire de contexte pour connexion de base de données
Un exemple plus réaliste — gérer une connexion de type base de données qui valide en cas de succès et effectue un rollback en cas d'erreur :
class ManagedTransaction:
def __init__(self, connection):
self.conn = connection
def __enter__(self):
self.conn.begin()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.conn.commit()
else:
self.conn.rollback()
return False # always let exceptions propagateLe motif — valider en cas de succès, effectuer un rollback en cas d'échec — apparaît dans toutes les vraies bibliothèques de bases de données (SQLite, SQLAlchemy, psycopg2 l'implémentent toutes).
contextlib.contextmanager : gestionnaires de contexte basés sur des générateurs
Écrire une classe complète avec __enter__ et __exit__ est la bonne approche pour les gestionnaires de contexte complexes ou à état. Pour les cas plus simples, le décorateur contextlib.contextmanager vous permet d'exprimer la même logique sous forme de fonction génératrice :
from contextlib import contextmanager
@contextmanager
def managed_open(path, mode="r", encoding="utf-8"):
print(f"Opening {path}")
f = open(path, mode, encoding=encoding)
try:
yield f # everything up to yield is __enter__
finally:
f.close() # everything after yield is __exit__
print(f"Closed {path}")
with managed_open("notes.txt", "w") as f:
f.write("hello\n")
# Opening notes.txt
# Closed notes.txtLe protocole de générateur se mappe directement sur le protocole de gestionnaire de contexte :
- Le code avant
yield→__enter__(initialisation). - L'expression
yield→ la valeur liée à la variableas. - Le code après
yield(généralement dans unfinally) →__exit__(nettoyage).
Le try/finally autour de yield est important : sans lui, une exception à l'intérieur du bloc with empêcherait l'exécution du code de nettoyage.
Exemple de contextmanager : répertoire de travail temporaire
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path):
original = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original)
with working_directory("/tmp"):
print(os.getcwd()) # /tmp (or system temp dir)
print(os.getcwd()) # restored to original directoryCe motif est également disponible dans la bibliothèque standard sous la forme tempfile.TemporaryDirectory.
Utilitaires de contextlib
Le module contextlib fournit plusieurs gestionnaires de contexte prêts à l'emploi qu'il vaut la peine de connaître :
contextlib.suppress
Supprime des exceptions spécifiques sans aucun code répétitif :
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("temp.txt") # no error even if file does not existÉquivalent à un try/except qui ne fait rien sur l'exception capturée.
contextlib.nullcontext
Un gestionnaire de contexte sans opération, utile lorsque vous voulez conditionnellement utiliser un gestionnaire de contexte ou non :
from contextlib import nullcontext
def process(data, lock=None):
ctx = lock if lock is not None else nullcontext()
with ctx:
return sorted(data)Sans nullcontext, vous auriez besoin d'une branche if lock: à chaque fois.
contextlib.ExitStack
ExitStack vous permet de gérer un nombre dynamique de gestionnaires de contexte — utile lorsque le nombre de ressources n'est pas connu avant l'exécution :
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [
stack.enter_context(open(f, "w", encoding="utf-8"))
for f in files
]
for i, fh in enumerate(handles):
fh.write(f"file {i}\n")
# All three files are closed hereExitStack est également l'outil approprié lorsque vous devez ajouter conditionnellement un gestionnaire de contexte, ou lorsque vous voulez différer le nettoyage à un moment ultérieur.
Quand utiliser with plutôt que Try/Finally
Utilisez with chaque fois que :
- Une ressource doit être libérée après utilisation (fichiers, sockets, verrous, curseurs de base de données).
- Vous voulez garantir le nettoyage même en cas d'exception.
- La logique de nettoyage est toujours la même qu'il y ait succès ou échec.
Utilisez un simple try/finally seulement lorsque :
- Vous avez besoin d'actions de nettoyage différentes selon le type d'exception — bien que
__exit__puisse le faire aussi. - Vous écrivez du code compatible Python 2 (rare aujourd'hui).
En pratique, si l'objet supporte le protocole de gestionnaire de contexte, préférez toujours with.
Référence rapide
| Fonctionnalité | Ce qu'elle fait |
|---|---|
with expr as v: | Appelle expr.__enter__(), lie le résultat à v, appelle __exit__ à la sortie |
| Ressources multiples | with A() as a, B() as b: — toutes deux nettoyées même si B() lève une exception |
__enter__(self) | Initialisation ; la valeur de retour est liée à la variable as |
__exit__(self, exc_type, exc_val, exc_tb) | Nettoyage ; retourner True pour supprimer l'exception |
@contextmanager | Transforme une fonction génératrice en gestionnaire de contexte |
contextlib.suppress(E) | Avale le type d'exception E sans try/except |
contextlib.nullcontext() | Espace réservé lorsqu'un gestionnaire de contexte est optionnel |
contextlib.ExitStack | Gère un ensemble dynamique ou conditionnel de gestionnaires de contexte |
Chapitres connexes
- Gestion des fichiers Python — utiliser
with open(...)pour chaque opération sur les fichiers - Python Try Except — gestion des exceptions et try/finally
- Les décorateurs Python —
@contextmanagerutilise le même motif de décorateur - Les générateurs Python — comment le protocole de générateur alimente
@contextmanager - Classes et objets Python — écrire les méthodes
__enter__et__exit__