Apprenez en lisant dans l'ordre

Instruction with et gestionnaires de contexte — Ouverture/fermeture sûre avec __enter__ / __exit__

Apprends l'instruction with et les gestionnaires de contexte en Python. Le couple __enter__ / __exit__ pour une ouverture/fermeture sûre, et comment écrire le tien — pratique inclus.

La dernière fois on a travaillé sur la protection de l'état interne d'une classe. Cette fois on s'éloigne d'une couche — vers les ressources externes qui vivent en dehors du processus Python (fichiers, connexions DB, sockets réseau, verrous) — et on regarde l'instruction with et les gestionnaires de contexte qui gèrent leur acquisition et libération en sécurité.

Pourquoi tu as besoin de l'instruction with

Les opérations comme ouvrir un fichier ou se connecter à une base de données viennent avec un devoir de nettoyage : « une fois fini, ferme-le ». Oublie de fermer, et tu fuites des descripteurs de fichier, tu retiens des connexions DB pour toujours, et tu laisses le processus externe s'accrocher aux ressources aussi.

Oublier de fermer retient aussi les ressources du processus externe
ProcessusPythonconnexionouverteMySQL / OSretient ressourceclose()libère les deux ✅Python(fini de tourner)connexiontoujours ouverteMySQL / OSretient encoreressourceFD épuisé /limite deconnexion ❌
Les fichiers et DB confient une ressource à l'OS ou un autre processus en dehors de Python. Si Python n'appelle pas close(), l'autre côté continue d'attendre des instructions et de retenir la ressource.

Tu peux écrire la même logique avec try / finally, mais alors chaque auteur doit se rappeler d'appeler close() dans finally à chaque fois. Quand le code grandit ou que plus de gens y touchent, quelqu'un oubliera — c'est juste la réalité.

L'instruction with enferme l'acquisition et la libération dans une unité syntaxique et les automatise. with open("file.txt") as f: est l'exemple canonique : le fichier est garanti de se fermer au moment où tu sors du bloc with.

Flux d'exécution de with X() as y:
with X() as y:entre__enter__appelévaleur de retourva dans ycorps dubloc__exit__nettoyagereturnsort
À l'entrée, __enter__ tourne et sa valeur de retour va dans la variable nommée après as. À la sortie — normale ou exceptionnelle — __exit__ est toujours appelé pour le nettoyage.

Écrire ton propre gestionnaire de contexte — __enter__ et __exit__

Un objet utilisable avec with est appelé un gestionnaire de contexte. Pour transformer une classe en un, implémente juste deux méthodes spéciales.

- __enter__(self) — tourne quand tu entres dans le bloc with. Sa valeur de retour est liée à la variable nommée après as.

- __exit__(self, exc_type, exc_val, traceback) — tourne quand tu sors du bloc. Toujours appelée, sortie normale ou exceptionnelle.

Un exemple minimal de style connexion DB est ci-dessous (on n'utilise pas vraiment une bibliothèque DB — on l'imite avec des chaînes).

class DatabaseManager:
    def __init__(self, db_name):
        self.db_name    = db_name
        self.connection = None       # pas encore connecté

    def __enter__(self):
        print(f"Connecting to {self.db_name}")
        self.connection = f"connection_to_{self.db_name}"   # vrai code : objet connexion réel
        return self.connection                              # valeur liée à la variable as

    def __exit__(self, exc_type, exc_val, traceback):
        print(f"Disconnecting from {self.db_name}")
        self.connection = None                              # nettoyage
        return False                                        # ne pas avaler les exceptions


with DatabaseManager("user_data_db") as conn:
    print(f"  active connection: {conn}")
    print("  inserting data")
# ↑ quand ce bloc sort, __exit__ tourne

Lance-le et la sortie apparaît dans l'ordre « connexion → travail dans le bloc → déconnexion ». La déconnexion tourne sans que personne l'appelle explicitement — c'est toute la valeur de with. Le développeur est libéré de l'inquiétude de fermer la connexion.

Écris le même DatabaseManager que dans le sample toi-même et pilote-le avec with.

① Définis class DatabaseManager: et affecte self.db_name = db_name et self.connection = None dans __init__(self, db_name).

② Définis __enter__(self). Imprime f"Connecting to {self.db_name}", fixe self.connection = f"connection_to_{self.db_name}", puis return self.connection.

③ Définis __exit__(self, exc_type, exc_val, traceback). Imprime f"Disconnecting from {self.db_name}", fixe self.connection = None, et finalement return False.

④ À l'intérieur de with DatabaseManager("user_data_db") as conn:, imprime f"active connection: {conn}" puis print("inserting data").

(Une fois que ça tourne correctement, l'explication apparaîtra.)

Éditeur Python

Exécuter le code pour voir le résultat

Les trois arguments de __exit__ — Capturer les exceptions

__exit__ prend trois arguments : exc_type, exc_val, traceback. Python les utilise pour dire à __exit__ si une exception est survenue dans le bloc with.

- Sortie normale — les trois sont None. Nettoie juste.

- Sortie sur exceptionexc_type est la classe d'exception, exc_val est l'instance, traceback est l'objet traceback.

La valeur de retour de __exit__ a aussi un sens. Retourner True avale l'exception — elle ne se propage pas au-delà du bloc. Retourner False / None la relève après le nettoyage. Le défaut devrait être False (ou ne rien return) : log ou notifie, mais laisse toujours l'exception s'échapper.

Valeurs concrètes livrées aux 3 arguments de __exit__
ce qui s'est passédans le withexc_typeexc_valtracebacksortie normale(pas d'exception)NoneNoneNoneraiseValueError("invalid")<class'ValueError'>ValueError('invalid')<tracebackobject>
Sortie normale livre None × 3. Les exceptions livrent le triplet « classe / instance / traceback ». Un raise ValueError("invalid") concret rend les contenus tangibles.
Sortie normale vs sortie sur exception dans __exit__
sortie normaledu withexc_type =None, etc.__exit__nettoyage seulsort dubloc withexceptiondans withexc_type / val/ traceback__exit__log + nettoyagereturn False-> propage
Quand le bloc lève, les informations de l'exception sont empaquetées dans les trois arguments de __exit__. La valeur de retour choisit entre avaler (True) ou relever (False).

Retourner True depuis __exit__ tue silencieusement l'exception

Si __exit__ retourne True, l'exception dans with ne se propage pas. Tentant, mais l'appelant pense maintenant que l'opération a réussi — c'est un effet de bord sérieux. Par défaut False (ou pas de return) : log ou notifie si tu veux, mais laisse toujours l'exception remonter.

Maintenant déclenche vraiment une exception dans with et observe ce qui atteint les trois arguments de __exit__.

Modifie le DatabaseManager de la Pratique 1 pour confirmer deux choses : __exit__ tourne même sur exception, et ses trois arguments reçoivent des valeurs.

① Définis class DatabaseManager: et affecte self.db_name = db_name dans __init__(self, db_name).

② Dans __enter__(self), imprime f"Enter: {self.db_name}" et return self (pour que as lie le manager lui-même).

③ Dans __exit__(self, exc_type, exc_val, traceback), imprime les trois arguments ligne par ligne (print("exc_type:", exc_type) et ainsi de suite), puis print(f"Exit: {self.db_name}"), et finalement return False.

④ Dans un bloc try:, ouvre with DatabaseManager("shop_db"):, imprime "start", puis raise ValueError("bad inventory data").

⑤ Capture avec except ValueError as e: et print(f"caught outside: {e}").

Éditeur Python

Exécuter le code pour voir le résultat

Comparé à try / finally — Pourquoi with gagne

Le travail d'un gestionnaire de contexte peut être fait avec try / finally. La raison de choisir with à la place est que « le couple ouverture/fermeture vit dans la classe ». Écrire la même tâche de deux manières rend la différence en volume de code de l'appelant et en clarté évidente.

# ❌ try / finally — l'appelant écrit le nettoyage à la main à chaque fois
db = DatabaseManager("shop_db")
conn = db.open()                      # méthode connect personnalisée
try:
    use(conn)                         # vrai travail
finally:
    db.close()                        # n'oublie pas — copié-collé partout


# ✅ with — l'ouverture/fermeture vit dans la classe, l'appelant ne fait que le travail
with DatabaseManager("shop_db") as conn:
    use(conn)                         # pas besoin de finally
with concentre la responsabilité ouverture/fermeture dans la classe
Si tu pars sur try / finally
  • Appelant — doit écrire try / finally à chaque fois
  • Oubli — un mauvais copier-coller et une fuite apparaît
  • Coût de changement — des étapes de nettoyage en plus signifient éditer chaque site d'appel
instruction with + gestionnaire de contexte
  • Appelant — une ligne, with X() as y:
  • Oubli — impossible au niveau syntaxique (__exit__ tourne toujours)
  • Coût de changement — du nettoyage en plus signifie éditer seulement __exit__
Séparer « utilisateur de la ressource » de « propriétaire de l'ouverture/fermeture » est la valeur que with ajoute. Quand les sites d'appel se multiplient, les changements de nettoyage restent dans une seule classe.

Utilise with partout où acquisition et libération vont par paires

Fichiers, connexions DB, verrous, sockets réseau — partout où tu « prends une ressource au début et dois la rendre à la fin » — est un candidat pour with. La bibliothèque standard Python expose déjà beaucoup de ces choses comme gestionnaires de contexte : open(), threading.Lock(), sqlite3.connect(), etc.

QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Dans with X() as y:, la valeur liée à y est la valeur de retour de quelle méthode ?

Question 2Quand une exception est levée dans un bloc with, qu'est-ce qui décrit correctement le comportement de __exit__ ?

Question 3Si __exit__ retourne True, qu'arrive-t-il à l'exception levée dans le bloc with ?