28. L’instruction with et les gestionnaires de contexte
Dans le chapitre 27, vous avez déjà utilisé l’instruction with pour travailler avec des fichiers. Elle vous a aidé à lire et à écrire des données sans vous soucier de fermer explicitement le fichier ensuite. À ce moment-là, cependant, l’accent était mis sur comment utiliser with, pas sur ce que cela signifie vraiment.
Dans ce chapitre, nous prenons du recul et regardons la situation dans son ensemble. Vous apprendrez ce que sont les gestionnaires de contexte(context managers), pourquoi gérer des ressources manuellement peut être risqué, et comment l’instruction with fournit un modèle sûr et fiable pour manipuler des ressources en Python. Vous verrez aussi que with ne se limite pas aux fichiers et vous acquerrez une compréhension conceptuelle de son fonctionnement en coulisses.
28.1) Ce que sont les gestionnaires de contexte sur le plan conceptuel
Un gestionnaire de contexte(context manager) est un objet qui définit ce qui doit se passer lorsque vous entrez et sortez d’un contexte particulier dans votre code. Voyez cela comme entrer et sortir d’une pièce : quand vous entrez, vous allumez la lumière ; quand vous sortez, vous l’éteignez — quoi qu’il arrive pendant que vous êtes à l’intérieur.
28.1.1) Le problème de la gestion des ressources
De nombreuses tâches de programmation impliquent d’acquérir une ressource, de l’utiliser, puis de la libérer :
# Ouvrir un fichier acquiert une ressource (descripteur de fichier)
file = open("data.txt", "r")
content = file.read()
# Utilisation du fichier...
file.close() # Libération de la ressourceCe schéma apparaît fréquemment :
- Ouvrir et fermer des fichiers
- Acquérir et libérer des verrous en programmation concurrente
- Ouvrir et fermer des connexions à une base de données
- Allouer et désallouer des buffers mémoire
Le défi est de s’assurer que la ressource est toujours libérée, même quand quelque chose se passe mal.
28.1.2) Ce qui fait d’un objet un gestionnaire de contexte
Un gestionnaire de contexte est n’importe quel objet qui implémente deux méthodes spéciales :
__enter__(): appelée lors de l’entrée dans le contexte (au début du blocwith)__exit__(): appelée lors de la sortie du contexte (à la fin du blocwith, même si une erreur survient)
Vous n’avez pas besoin d’implémenter ces méthodes vous-même pour utiliser des gestionnaires de contexte — les types intégrés de Python comme les objets fichier les possèdent déjà. Comprendre ce concept vous aide à reconnaître quand vous travaillez avec un gestionnaire de contexte.
# Les objets fichier sont des gestionnaires de contexte
# Ils ont des méthodes __enter__ et __exit__
file = open("example.txt", "r")
print(hasattr(file, "__enter__")) # Output: True
print(hasattr(file, "__exit__")) # Output: True
file.close()28.1.3) Le schéma de base : initialisation, utilisation, nettoyage
Les gestionnaires de contexte suivent un schéma en trois phases :
Phase d’initialisation : acquérir la ressource (par exemple, ouvrir un fichier, se connecter à une base de données, acquérir un verrou)
Phase d’utilisation : travailler avec la ressource (par exemple, lire/écrire un fichier, interroger une base, accéder à des données partagées)
Phase de nettoyage : libérer la ressource (par exemple, fermer le fichier, se déconnecter de la base, relâcher le verrou)
L’idée clé : la phase de nettoyage a toujours lieu, quoi qu’il arrive pendant la phase d’utilisation.
28.2) Pourquoi la gestion manuelle des ressources est risquée
Avant d’apprendre l’instruction with, comprenons pourquoi la gestion manuelle des ressources peut échouer et causer des problèmes.
28.2.1) L’oubli de fermeture
L’erreur la plus courante consiste simplement à oublier de fermer une ressource :
# Lecture d’un fichier de configuration
config_file = open("config.txt", "r")
settings = config_file.read()
# Oups ! Oubli de fermer le fichier
# Le descripteur de fichier reste ouvertMême si Python finit par fermer les fichiers quand le programme se termine, laisser des fichiers ouverts peut causer des problèmes :
- Épuisement des ressources : les systèmes d’exploitation limitent le nombre de fichiers ouverts
- Verrouillage de fichier : d’autres programmes pourraient ne pas pouvoir accéder au fichier
- Perte de données : les écritures en tampon pourraient ne pas être vidées sur le disque
28.2.2) Les erreurs empêchent le nettoyage
Même quand vous pensez à fermer les ressources, des erreurs peuvent empêcher le code de nettoyage de s’exécuter :
# Tentative de traitement d’un fichier
data_file = open("data.txt", "r")
content = data_file.read()
result = process_data(content) # Et si cela lève une erreur ?
data_file.close() # Cette ligne ne s’exécute jamais si process_data() échoue !Si process_data() lève une exception, le programme saute directement à la gestion d’erreur, en ignorant l’appel à close(). Le fichier reste ouvert indéfiniment.
28.2.3) Plusieurs points de sortie
Les fonctions avec plusieurs instructions return rendent le nettoyage encore plus difficile :
def read_first_valid_line(filename):
file = open(filename, "r")
for line in file:
line = line.strip()
if line and not line.startswith("#"):
# Ligne valide trouvée - mais le fichier est toujours ouvert !
return line
file.close() # Atteint uniquement si aucune ligne valide n’est trouvée
return NoneLa fonction retourne tôt lorsqu’elle trouve une ligne valide, en laissant le fichier ouvert. Il faudrait ajouter file.close() avant chaque return — facile à oublier et difficile à maintenir.
28.2.4) Gestion d’erreurs complexe
Vous pourriez essayer d’utiliser try-except-finally pour garantir le nettoyage :
# Tentative de gérer correctement les erreurs
file = None
try:
file = open("data.txt", "r")
content = file.read()
result = process_data(content)
except FileNotFoundError:
print("File not found")
except ValueError:
print("Invalid data format")
finally:
if file is not None:
file.close()Cela fonctionne, mais c’est verbeux et sujet aux erreurs. Vous devez :
- Initialiser la variable avant le bloc try
- Vérifier si la ressource a été acquise avec succès avant de fermer
- Penser à inclure le bloc finally
- Répéter ce schéma pour chaque ressource
28.2.5) L’impact dans le monde réel
Ces problèmes ne sont pas seulement théoriques. Considérez un programme qui traite des milliers de fichiers :
# AVERTISSEMENT : fuite de ressources - uniquement à des fins de démonstration
# PROBLÈME : les fichiers ne sont jamais fermés
def process_many_files(filenames):
results = []
for filename in filenames:
file = open(filename, "r") # Ouvre un fichier
data = file.read()
results.append(analyze(data))
# ERREUR : ne ferme jamais le fichier
return results
# Après avoir traité 1000 fichiers, vous avez 1000 descripteurs de fichiers ouverts !
# Au bout d’un moment, l’OS refuse d’ouvrir davantage de fichiersOutput (after many iterations):
OSError: [Errno 24] Too many open files: 'file_1001.txt'Le programme plante parce qu’il a épuisé la limite du système pour les descripteurs de fichiers. C’est une fuite de ressources — des ressources sont acquises mais jamais libérées.
28.3) Utiliser with au-delà des fichiers
L’instruction with fonctionne avec n’importe quel gestionnaire de contexte, pas seulement avec les fichiers. Explorons comment elle résout les problèmes que nous avons identifiés et voyons-la utilisée dans divers contextes.
28.3.1) Syntaxe de base de l’instruction with
L’instruction with a une structure simple :
with expression as variable:
# Bloc de code qui utilise la ressource
# Indenté sous l’instruction with
# Ressource libérée automatiquement iciL’expression doit s’évaluer en un objet gestionnaire de contexte. La partie as variable est optionnelle mais généralement incluse — elle vous donne un nom pour référencer la ressource.
28.3.2) Utiliser with pour les opérations sur fichiers
Voici comment l’instruction with transforme la gestion des fichiers :
# Approche manuelle (risquée)
file = open("data.txt", "r")
content = file.read()
file.close()
# Approche avec l’instruction with (sûre)
with open("data.txt", "r") as file:
content = file.read()
# Fichier fermé automatiquement ici, même si une erreur survientLe fichier est garanti d’être fermé à la fin du bloc with, que le code se termine normalement ou lève une exception.
28.3.3) Plusieurs gestionnaires de contexte
Vous pouvez gérer plusieurs ressources dans une seule instruction with :
# Lire depuis un fichier et écrire dans un autre
with open("input.txt", "r") as input_file, open("output.txt", "w") as output_file:
for line in input_file:
processed = line.upper()
output_file.write(processed)
# Les deux fichiers sont fermés automatiquement iciCela équivaut à imbriquer des instructions with, mais c’est plus concis :
# Instructions with imbriquées (équivalent mais plus verbeux)
with open("input.txt", "r") as input_file:
with open("output.txt", "w") as output_file:
for line in input_file:
processed = line.upper()
output_file.write(processed)Les deux approches garantissent que les deux fichiers sont correctement fermés, même si une erreur survient pendant le traitement.
28.3.4) Travailler avec des fichiers compressés
Le module gzip de Python fournit des gestionnaires de contexte pour lire et écrire des fichiers compressés :
import gzip
# Écriture de données compressées
with gzip.open("data.txt.gz", "wt") as compressed_file:
compressed_file.write("This text will be compressed\n")
compressed_file.write("Saving space on disk\n")
# Fichier fermé automatiquement et compression finalisée
# Lecture de données compressées
with gzip.open("data.txt.gz", "rt") as compressed_file:
content = compressed_file.read()
print(content)Output:
This text will be compressed
Saving space on diskL’instruction with garantit que le fichier compressé est correctement finalisé, ce qui est crucial pour la compression — une compression incomplète peut produire des fichiers corrompus.
28.3.5) Changer de répertoire temporairement
Quand vous devez changer temporairement le répertoire de travail courant, la gestion manuelle peut être risquée :
import os
# Répertoire courant
print(f"Starting in: {os.getcwd()}")
# Changement manuel de répertoire (risqué)
original_dir = os.getcwd()
os.chdir("/tmp")
print(f"Now in: {os.getcwd()}")
process_files() # Si une erreur survient ici, il se peut que nous ne revenions pas à original_dir
os.chdir(original_dir)Si process_files() lève une exception, le programme ne revient jamais au répertoire original, ce qui peut provoquer un comportement inattendu dans le code qui suit.
Python 3.11 a introduit contextlib.chdir(), un gestionnaire de contexte qui garantit le retour au répertoire d’origine :
import os
from contextlib import chdir
print(f"Starting in: {os.getcwd()}")
# Utilisation du gestionnaire de contexte (sûr)
with chdir("/tmp"):
print(f"Temporarily in: {os.getcwd()}")
process_files() # Même si cela lève une erreur, on revient au répertoire d’origine
print(f"Back in: {os.getcwd()}")
# Retour automatique au répertoire d’origineLe changement de répertoire est automatiquement annulé lorsque le bloc with se termine, que le code se termine normalement ou lève une exception.
28.3.6) Verrous de thread pour la programmation concurrente
En programmation concurrente (couverte dans des sujets avancés), les verrous sont des gestionnaires de contexte :
# Exemple conceptuel (nous apprendrons threading dans les sujets avancés)
import threading
lock = threading.Lock()
# Gestion manuelle du verrou (risquée)
lock.acquire()
# Section critique - et si une erreur survient ?
lock.release() # Peut ne pas s’exécuter
# Instruction with (sûre)
with lock:
# Section critique
# Verrou libéré automatiquement, même si une erreur survient
pass28.4) L’instruction with sous le capot (conceptuel uniquement)
Comprendre comment l’instruction with fonctionne en interne vous aide à apprécier sa puissance et à reconnaître quand vous travaillez avec des gestionnaires de contexte. Cette section fournit une vue d’ensemble conceptuelle — vous n’avez pas besoin d’implémenter ces détails vous-même.
28.4.1) Les deux méthodes spéciales
Chaque gestionnaire de contexte implémente deux méthodes spéciales que Python appelle automatiquement :
__enter__(self) : appelée quand le bloc with commence
- Effectue des opérations d’initialisation (ouverture de fichiers, acquisition de verrous, etc.)
- Renvoie l’objet ressource qui est affecté à la variable après
as - Si aucune clause
asn’est présente, la valeur de retour est ignorée
__exit__(self, exc_type, exc_value, traceback) : appelée quand le bloc with se termine
- Effectue des opérations de nettoyage (fermeture de fichiers, libération de verrous, etc.)
- Reçoit des informations sur toute exception survenue
- Est toujours appelée, même si une exception a été levée
- Peut supprimer des exceptions en renvoyant
True(rarement utilisé)
28.4.2) Comment Python exécute une instruction with
Traçons ce qui se passe lorsque Python exécute une instruction with :
with open("data.txt", "r") as file:
content = file.read()
print(content)Voici l’exécution étape par étape :
Étape 1 : Python évalue open("data.txt", "r"), en créant un objet fichier
Étape 2 : Python appelle la méthode __enter__() de l’objet fichier
Étape 3 : __enter__() renvoie l’objet fichier lui-même, qui est affecté à file
Étape 4 : Python exécute le bloc de code indenté
Étape 5 : Quand le bloc se termine (normalement ou à cause d’une exception), Python appelle __exit__()
Étape 6 : __exit__() ferme le fichier et effectue le nettoyage
Étape 7 : Si une exception s’est produite, Python la relance après le nettoyage
28.4.3) Gestion des exceptions dans les gestionnaires de contexte
Quand une exception survient à l’intérieur d’un bloc with, Python transmet des informations à __exit__() :
# Ce qui se passe quand une erreur survient
try:
with open("data.txt", "r") as file:
content = file.read()
result = int(content) # Peut lever ValueError
print(result)
except ValueError as e:
print(f"Invalid data: {e}")
# Le fichier est fermé avant l’exécution du bloc exceptFlux d’exécution quand ValueError se produit :
Le point clé : __exit__() est appelée avant la propagation de l’exception, garantissant que le nettoyage se produit même quand des erreurs surviennent.
28.4.4) Un modèle mental simple
Considérez l’instruction with comme une garantie :
with resource_manager as resource:
# Utiliser la ressource
pass
# Python GARANTIT que le nettoyage a eu lieuQuoi qu’il arrive dans le bloc — fin normale, instruction return, exception, ou même erreurs système — Python appelle __exit__() pour nettoyer. Cette garantie est ce qui rend with si puissante et pourquoi vous devriez l’utiliser dès que vous travaillez avec des ressources.
Points clés à retenir de ce chapitre :
- Les gestionnaires de contexte(context managers) définissent des opérations d’initialisation et de nettoyage pour les ressources
- La gestion manuelle des ressources est risquée à cause des nettoyages oubliés, des erreurs et de multiples points de sortie
- L’instruction
withgarantit que le nettoyage a lieu, même quand des erreurs surviennent - Utilisez
withpour les fichiers et toute autre ressource nécessitant un nettoyage - Plusieurs ressources peuvent être gérées dans une seule instruction
with - Sous le capot,
withappelle automatiquement les méthodes__enter__()et__exit__() __exit__()s’exécute toujours, garantissant que les ressources sont correctement libérées
L’instruction with transforme la gestion des ressources, d’un travail manuel sujet aux erreurs, en un nettoyage automatique et fiable. Utilisez-la dès que vous travaillez avec des fichiers, des connexions à des bases de données, des verrous, ou toute autre ressource nécessitant un nettoyage correct. Votre code sera plus sûr, plus propre et plus professionnel.