Module : Gestionnaires de Contexte et l'instruction with
1. Quoi : L'instruction with
L'instruction with en Python est utilisée pour envelopper l'exécution d'un bloc de code. Elle simplifie la gestion des ressources (comme les fichiers, les connexions réseau ou les verrous de thread) en garantissant que les opérations de "nettoyage" sont effectuées, même si des erreurs se produisent.
Vous l'avez probablement déjà rencontrée pour la manipulation de fichiers :
with open('my_file.txt', 'w') as f:
f.write('Hello, World!')
# À la sortie de ce bloc, le fichier 'f' est automatiquement fermé.
Cette syntaxe est plus propre et plus sûre que la gestion manuelle avec un bloc try...finally.
2. Pourquoi : Une gestion de ressources robuste
Le principal avantage de with est de garantir que les ressources sont correctement libérées.
- Fermeture automatique : Les fichiers sont fermés, les connexions réseau sont terminées, les verrous sont libérés.
- Gestion des erreurs : Le nettoyage a lieu même si une exception est levée à l'intérieur du bloc
with. - Code plus lisible : La logique d'acquisition et de libération de la ressource est encapsulée, rendant le code principal plus clair.
3. Comment : Le Protocole de Gestion de Contexte
Pour qu'un objet puisse être utilisé avec l'instruction with, il doit implémenter le protocole de gestion de contexte, qui consiste en deux méthodes spéciales :
__enter__(self): Cette méthode est appelée au début du blocwith. Sa valeur de retour est généralement assignée à la variable après leas(mais c'est optionnel).__exit__(self, exc_type, exc_val, exc_tb): Cette méthode est appelée à la fin du blocwith, que ce soit normalement ou à cause d'une exception.- Si le bloc se termine sans erreur, les trois arguments
exc_*serontNone. - Si une exception se produit,
exc_type,exc_val, etexc_tbcontiendront les informations sur l'exception. - Si la méthode
__exit__retourneTrue, l'exception est "supprimée" (elle ne sera pas propagée). Si elle retourneFalse(ouNone), l'exception continue sa propagation après le nettoyage.
- Si le bloc se termine sans erreur, les trois arguments
A. Créer un gestionnaire de contexte avec une classe
Exemple : Un minuteur simple
Créons un gestionnaire de contexte qui mesure le temps passé à l'intérieur du bloc with.
import time
class Timer:
def __init__(self, description):
self.description = description
def __enter__(self):
print(f"Starting '{self.description}'...")
self.start_time = time.time()
# On ne retourne rien, donc pas de 'as' nécessaire
return self
def __exit__(self, exc_type, exc_val, exc_tb):
end_time = time.time()
elapsed = end_time - self.start_time
print(f"'{self.description}' finished in {elapsed:.4f} seconds.")
# On ne gère pas les exceptions, donc on retourne None (implicitement)
# Utilisation
with Timer("Long operation"):
# Simule un travail
time.sleep(2)
# Affiche:
# Starting 'Long operation'...
# 'Long operation' finished in 2.00xx seconds.
B. Créer un gestionnaire de contexte avec contextlib
Écrire une classe complète peut être verbeux. Le module contextlib de la bibliothèque standard fournit un décorateur, @contextmanager, qui permet de créer un gestionnaire de contexte à partir d'une simple fonction génératrice.
- Le code avant l'instruction
yieldcorrespond à__enter__. - L'instruction
yieldproduit la valeur qui sera assignée à la variableas. - Le code après l'instruction
yieldcorrespond à__exit__.
Exemple : Le minuteur réécrit avec @contextmanager
import time
from contextlib import contextmanager
@contextmanager
def timer(description):
print(f"Starting '{description}'...")
start_time = time.time()
try:
# Le contrôle est passé au bloc 'with' ici
yield
finally:
# Ce code est exécuté à la sortie du bloc 'with'
end_time = time.time()
elapsed = end_time - start_time
print(f"'{description}' finished in {elapsed:.4f} seconds.")
# Utilisation
with timer("Another long operation"):
time.sleep(1.5)
Le try...finally est crucial pour garantir que le code de nettoyage s'exécute même en cas d'exception dans le bloc with. Cette version est souvent considérée comme plus concise et plus lisible.
4. Cas d'usage
- Gestion de fichiers : Le cas le plus courant.
- Connexions à des bases de données :
__enter__ouvre la connexion et démarre une transaction,__exit__la ferme ou la valide/annule. - Verrous de concurrence (
threading.Lock) :with my_lock:acquiert le verrou au début et le libère à la fin, quoi qu'il arrive. - Changement temporaire de contexte : Changer temporairement un paramètre de configuration et le restaurer à sa valeur d'origine à la fin.
- Suppression d'exceptions.
L'instruction with est un outil puissant pour écrire du code Python plus sûr et plus déclaratif.
Exercice 01 : Gestionnaire de Contexte pour les Connexions à une Base de Données
Objectif
Cet exercice a pour but de vous faire créer un gestionnaire de contexte simple en utilisant une classe pour simuler la gestion d'une connexion à une base de données. Cela illustre comment with peut garantir que les ressources sont correctement ouvertes et fermées.
Contexte
Lorsque vous travaillez avec des bases de données, il est crucial de s'assurer que chaque connexion ouverte est correctement fermée à la fin, même si des erreurs se produisent. Un gestionnaire de contexte est la solution parfaite pour ce problème.
Vous allez simuler ce comportement en créant une classe DatabaseConnection qui pourra être utilisée avec l'instruction with.
Énoncé
-
Créez un nouveau fichier Python nommé
db_context_manager.py. -
Définissez la classe
DatabaseConnection.- Son constructeur
__init__doit accepter un paramètredb_name(le nom de la base de données). - Dans le constructeur, stockez
db_nameet initialisez un attributself.is_connectedàFalse.
- Son constructeur
-
Implémentez la méthode
__enter__.- Cette méthode simule l'ouverture de la connexion.
- Elle doit afficher un message comme
f"Connecting to database '{self.db_name}'...". - Elle doit passer
self.is_connectedàTrue. - Elle doit se retourner elle-même (
return self) pour que l'objet puisse être utilisé dans le blocwith(avec la syntaxeas).
-
Implémentez la méthode
__exit__.- Cette méthode simule la fermeture de la connexion.
- Elle doit afficher un message comme
f"Closing connection to database '{self.db_name}'...". - Elle doit passer
self.is_connectedàFalse. - Elle ne doit pas supprimer les exceptions (elle doit donc retourner
NoneouFalseimplicitement).
-
Ajoutez une méthode
queryà votre classe.- Elle prend un argument
sql_query. - Elle doit vérifier si
self.is_connectedestTrue. - Si oui, elle affiche
f"Executing query: '{sql_query}'". - Si non, elle lève une
ConnectionErroravec un message clair.
- Elle prend un argument
-
Testez votre gestionnaire de contexte : a. Utilisez une instruction
withpour créer une connexion à une base de données "test_db". b. À l'intérieur du blocwith, appelez la méthodequeryavec une requête SQL de votre choix. c. Après le blocwith, essayez d'appeler à nouveau la méthodequerysur l'objet connexion pour vérifier que la connexion est bien fermée et qu'une erreur est levée. Utilisez un bloctry...exceptpour attraper cetteConnectionError.
Résultat Attendu
Connecting to database 'test_db'...
Executing query: 'SELECT * FROM users'
Closing connection to database 'test_db'...
---
Trying to query after closing connection...
Closing connection to database 'another_db'...
Error: Not connected to the database.
Cliquez ici pour voir un exemple de code de solution
# db_context_manager.py
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.is_connected = False
def __enter__(self):
"""Opens the database connection."""
print(f"Connecting to database '{self.db_name}'...")
self.is_connected = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Closes the database connection."""
print(f"Closing connection to database '{self.db_name}'...")
self.is_connected = False
# We don't handle exceptions, so we let them propagate by returning None/False.
def query(self, sql_query):
"""Executes a SQL query."""
if not self.is_connected:
raise ConnectionError("Not connected to the database.")
print(f"Executing query: '{sql_query}'")
# --- Testing ---
# 1. Normal usage with the 'with' statement
with DatabaseConnection("test_db") as db:
db.query("SELECT * FROM users")
print("---")
# 2. Trying to use the connection outside the 'with' block
print("Trying to query after closing connection...")
# We need to re-create the object to test this, as 'db' from the previous
# block still exists but is in a "closed" state.
db_instance = DatabaseConnection("another_db")
# Let's manually enter and exit to simulate the state after a 'with' block
db_instance.__enter__()
db_instance.__exit__(None, None, None) # Manually close it
try:
db_instance.query("SELECT * FROM products")
except ConnectionError as e:
print(f"Error: {e}")