Skip to main content
Niveau : Avancé

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 bloc with. Sa valeur de retour est généralement assignée à la variable après le as (mais c'est optionnel).
  • __exit__(self, exc_type, exc_val, exc_tb) : Cette méthode est appelée à la fin du bloc with, que ce soit normalement ou à cause d'une exception.
    • Si le bloc se termine sans erreur, les trois arguments exc_* seront None.
    • Si une exception se produit, exc_type, exc_val, et exc_tb contiendront les informations sur l'exception.
    • Si la méthode __exit__ retourne True, l'exception est "supprimée" (elle ne sera pas propagée). Si elle retourne False (ou None), l'exception continue sa propagation après le nettoyage.

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 yield correspond à __enter__.
  • L'instruction yield produit la valeur qui sera assignée à la variable as.
  • Le code après l'instruction yield correspond à __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é

  1. Créez un nouveau fichier Python nommé db_context_manager.py.

  2. Définissez la classe DatabaseConnection.

    • Son constructeur __init__ doit accepter un paramètre db_name (le nom de la base de données).
    • Dans le constructeur, stockez db_name et initialisez un attribut self.is_connected à False.
  3. 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 bloc with (avec la syntaxe as).
  4. 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 None ou False implicitement).
  5. Ajoutez une méthode query à votre classe.

    • Elle prend un argument sql_query.
    • Elle doit vérifier si self.is_connected est True.
    • Si oui, elle affiche f"Executing query: '{sql_query}'".
    • Si non, elle lève une ConnectionError avec un message clair.
  6. Testez votre gestionnaire de contexte : a. Utilisez une instruction with pour créer une connexion à une base de données "test_db". b. À l'intérieur du bloc with, appelez la méthode query avec une requête SQL de votre choix. c. Après le bloc with, essayez d'appeler à nouveau la méthode query sur l'objet connexion pour vérifier que la connexion est bien fermée et qu'une erreur est levée. Utilisez un bloc try...except pour attraper cette ConnectionError.

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}")