Définition
Un programme qui ouvre un fichier contracte une dette : il devra le refermer. Tant que tout se passe bien, elle se règle quelques lignes plus bas et personne n'y pense. Mais si une erreur survient au milieu du traitement, l'exécution saute par-dessus la ligne de fermeture et le fichier reste ouvert dans le dos du programme. Sur un script lancé à la main, cela ne se voit pas ; sur un serveur qui tourne des semaines, les ressources abandonnées s'accumulent jusqu'à la panne.
Un gestionnaire de contexte répond à cet ennui. C'est un objet qui prépare une ressource quand le programme entre dans un bloc et la remet en ordre quand il en sort. Il s'emploie avec le mot-clé with, et sa promesse est simple : le nettoyage aura lieu, que le bloc se termine normalement ou qu'il échoue en cours de route.
with open("rapport.txt", "w") as fichier:
fichier.write("Chiffres du mois")
# Ici, le fichier est déjà referméCes lignes ne font pas qu'écourter le code, elles déplacent une responsabilité : ce n'est plus à celui qui utilise le fichier de penser à la fermeture, c'est à l'objet fichier de la garantir. Sinon, il faudrait fermer dans un finally et refaire ce montage partout où la ressource sert.
Le mécanisme, sans magie
Comment with sait-il quoi fermer, alors qu'il ne connaît rien aux fichiers ? Il n'en sait rien, précisément : il appelle deux méthodes que l'objet doit fournir, __enter__ et __exit__, deux membres de la famille décrite par le terme méthode magique. Tout objet qui les expose devient utilisable dans un bloc with, fichier, connexion à une base ou simple chronomètre.
Le tableau ci-dessous déroule un bloc du début à la fin : le moment, ce que Python déclenche seul, et ce que vous en percevez.
| Moment | Ce que Python appelle | Ce que vous en voyez |
|---|---|---|
| Entrée dans le bloc | __enter__ | La valeur reçue après as |
| Corps du bloc | rien | Votre code, inchangé |
| Sortie normale | __exit__(None, None, None) | Le nettoyage, en silence |
| Sortie sur erreur | __exit__(classe, valeur, trace) | Le nettoyage, puis l'erreur qui remonte |
La dernière ligne est la plus instructive. Quand le bloc échoue, Python passe à __exit__ trois arguments qui décrivent l'incident : la classe de l'erreur, l'objet levé et la trace des appels. Ces trois valeurs valent None lorsque tout s'est bien passé.
La méthode sait donc dans quelle situation elle travaille, ce qui rend le protocole utile au-delà du nettoyage : valider une transaction quand le bloc a réussi, l'annuler quand il a échoué.
Écrire le sien
Rien n'oblige à se contenter des gestionnaires livrés avec Python. Deux chemins mènent au même résultat : une classe qui implémente les deux méthodes réservées, ou un générateur transformé en gestionnaire par un décorateur de la bibliothèque standard, plus court et bien plus fréquent.
from contextlib import contextmanager
import time
@contextmanager
def chronometre(etape):
debut = time.perf_counter() # avant le yield : le rôle de __enter__
try:
yield # ici s'exécute le corps du bloc with
finally:
duree = time.perf_counter() - debut
print(etape, "a pris", round(duree, 3), "secondes")
with chronometre("chargement"):
charger_les_donnees()La convention se lit en une fois : ce qui précède le yield joue le rôle de __enter__, ce qui le suit celui de __exit__, et le yield marque l'endroit où le corps du bloc s'intercale.
Le try n'est pas décoratif. Si le bloc lève une erreur, Python la renvoie dans le générateur, à la ligne du yield, où la fonction s'arrêterait. Sans finally, le chronomètre resterait muet le jour précis où sa mesure servirait le plus, celui où le chargement a échoué.
Le module contextlib ne se limite pas à ce décorateur : il fournit aussi suppress, qui ignore volontairement une erreur nommée, et ExitStack, qui empile des ressources dont le nombre n'est connu qu'à l'exécution.
Deux pièges qui coûtent une soirée
Le premier vient d'un return de trop. Une méthode __exit__ qui renvoie une valeur vraie annonce à Python que l'erreur a été traitée : l'exception s'arrête net, sans remontée ni trace dans les journaux.
class Connexion:
def __exit__(self, classe, valeur, trace):
self.fermer()
return True # avale toutes les erreurs du bloc
# Correct : ne rien renvoyer, ou renvoyer FalseRenvoyer True par réflexe transforme un gestionnaire de contexte en tapis sous lequel les incidents sont glissés : le programme continue avec une transaction à moitié écrite, et le défaut se manifeste des heures plus tard, loin de sa cause. N'avalez une erreur que si vous savez laquelle, et pourquoi.
Le second piège concerne la variable nommée après as. Elle ne disparaît pas à la fin du bloc, car Python ne crée pas de portée locale ici. Ce qui a disparu, c'est la ressource derrière le nom : relire le fichier une ligne plus bas lève une ValueError, alors que la variable, elle, semble parfaitement valide. Le réflexe : tout ce qui a besoin de la ressource vit à l'intérieur du bloc.
Questions fréquentes
Peut-on ouvrir plusieurs ressources dans un seul bloc ?
Oui, en les séparant par des virgules, et depuis Python 3.10 en les entourant de parenthèses pour les répartir sur plusieurs lignes. La fermeture se fait dans l'ordre inverse de l'ouverture, ce qui compte dès que la seconde ressource dépend de la première.
Faut-il encore entourer le bloc d'un try/except ?
Oui, dès que l'erreur doit être traitée. Le protocole garantit le nettoyage, pas le rattrapage : l'exception remonte quand même après la fermeture, et except reste le seul endroit où décider quoi en faire. Les deux mécanismes répondent à deux questions différentes.
Existe-t-il une version pour le code asynchrone ?
Oui : async with, qui appelle __aenter__ et __aexit__ au lieu des deux méthodes habituelles, et autorise une attente pendant l'ouverture comme pendant la fermeture. Indispensable quand la ressource est une session réseau qu'on ne libère pas instantanément.