> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Au-delà du "with": les gestionnaires de contexte
- URL: https://www.sfeir.dev/back/au-dela-du-with-les-gestionnaires-de-contexte-en-python/
- Published: 2023-07-07T08:56:27.000Z
- Updated: 2023-11-02T11:57:31.000Z
- Description: Plongeons dans les gestionnaires de contexte en Python. L'instruction with n'aura plus de secret pour vous !
- Author: Alexandre Moevi
- Tags: Back, Python

Si vous avez déjà codé en Python, il est fort probable que vous ayez utilisé le mot-clé `with` de cette manière :

```python
with open("sfeir.txt") as file:  
	data = file.read()
	print(data)

```

Cependant, le fonctionnement précis de l'instruction `with` reste souvent méconnu. En jetant un regard en coulisses et en explorant les gestionnaires de contexte sous-jacents, nous allons découvrir les mécanismes qui donnent vie à cette instruction.

## Contexte ?

L'instruction `with` offre une méthode simple et puissante pour gérer les ressources à l'aide de gestionnaires de contexte. Cette approche suit le patron de conception classique `try... except... finally` et permet d'encapsuler l'exécution d'un bloc de code de manière propre et sécurisée.

Un gestionnaire de contexte (ou *context manager*) gère donc l'acquisition et la libération de ressources dans un bloc de code. Il a pour but de garantir que les ressources sont correctement initialisées et nettoyées, même en cas d'erreur ou d'exception.

Un context manager doit implémenter deux méthodes spéciales : `__enter__()` et `__exit__()`. La méthode `__enter__()` est appelée au début du bloc `with` et est responsable de l'initialisation des ressources. Elle renvoie généralement l'objet qui sera associé au gestionnaire de contexte. La méthode `__exit__()` est appelée à la fin du bloc `with` et est responsable de la libération des ressources.

Avec ce premier gestionnaire basique, on voit clairement l'ordre d'exécution.

```python
class MyContextManager:
    def __enter__(self):
        print("Initialisation des ressources")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Nettoyage des ressources")

with MyContextManager() as cm:
    print("Exécution du bloc de code")

```

```shell
$ python contexte.py
Initialisation des ressources
Exécution du bloc de code
Nettoyage des ressources

```

Ainsi, on peut s'amuser à re-coder un context manager qui va lire ou écrire dans des fichiers. On utilse la fonction `open()` qui nous renvoie un flux de données[\[1\]](#fn1). Le flux est ouvert dans la méthode `__enter__()` et on n'oublie pas de le fermer dans la fonction `__exit__()`.

```python
class Fichier:
    def __init__(self, nom, mode):
        self.nom = nom
        self.mode = mode
    def __enter__(self):
        self.fichier = open(self.nom, self.mode)
        return self.fichier
    def __exit__(self, exc_type, exc_value, traceback):
        self.fichier.close()

with Fichier("sfeir.txt", 'w') as f:
    f.write("sfeir.dev")

```

## La classe, non ?

Python fournit également le module `contextlib` qui propose un décorateur `@contextmanager` afin de créer des gestionnaires de contexte de manière plus concise, sans avoir à définir une classe complète. Si on remplace notre classe `Fichier`, ça donne ceci :

```python
from contextlib import contextmanager

@contextmanager
def fichier(nom, mode):
    try:
        f = open(nom, mode)
        yield f
    finally:
        f.close()

with fichier("sfeir.txt", 'w') as f:
    f.write("sfeir.dev")

```

Avec le décorateur, on transforme la fonction `fichier()` en un gestionnaire de contexte. La fonction utilise le mot-clé `yield` pour indiquer le point d'entrée et de sortie du contexte. Autrement dit, on prépare les ressources nécessaires pour le contexte avant `yield` puis on les détruit après.

Dans notre code, on utilise `yield` pour retourner le flux qui sera utilisé à l'intérieur du bloc `with`. Une fois le bloc terminé, la partie `finally` est exécutée pour fermer le fichier.

## Les context managers en action

On retrouve les gestionnaires de contexte aussi bien dans la librairie standard de Python que dans les modules populaires. Voici quelques exemples supplémentaires de gestionnaires de contexte dans différents domaines.

```python
# Gestion d'une connexion vers une base données
import sqlite3

with sqlite3.connect("database.db") as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM table")
    results = cursor.fetchall()
    print(results)

# Gestion d'une session dans le module requests
# Une session conserve les paramètres et les cookies entre plusieurs requêtes
import requests

with requests.Session() as session:
    # Effectuer des requêtes HTTP ici
    response = session.get("https://www.sfeir.dev")
    print(response.status_code)

# Gestion d'une connexion vers un Redis    
import redis

with redis.Redis(host='localhost', port=6379) as r:
    r.set('cle', 'valeur')
    valeur = r.get('cle')
    print(valeur)

# Gestion de threads concurrents
# Ici, 10 threads doivent incrémenter la même variable
# Sans le lock, les threads peuvent accéder simultanément à la variable partagée,
# ce qui peut entraîner des incohérences et des erreurs de valeur.
# Le lock permet d'assurer qu'un seul thread à la fois peut modifier la variable,
# garantissant ainsi la cohérence des données.
import threading

lock = threading.Lock()
counter = 0

def thread_function():
    global counter
    thread_name = threading.current_thread().name
    for _ in range(10):
        with lock:
            counter += 1
            print(f"{thread_name} incrémente, valeur du compteur : {counter}")

threads = [threading.Thread(target=thread_function, name=f"Thread {i}") for i in range(1, 11)]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

```

## Niveau supérieur

Pour aller plus loin, les gestionnaires peuvent se souvenir de l'état précédent et adapter leur comportement en fonction du contexte. Cela permet de créer des contextes imbriqués où certaines actions sont effectuées à l'entrée et à la sortie de chaque niveau de contexte. On parle alors de gestionnaire réentrant. Voici un exemple de ce type de context manager :

```python
class Indentation:
    def __init__(self):
        self.niveau = 0
    
    def __enter__(self):
        self.niveau += 1
        return self
    
    def __exit__(self, exc_type, exc_val, traceback):
        self.niveau -= 1
    
    def imprimer(self, texte):
        print(f"{'    ' * self.niveau}{texte}")

with Indentation() as indent:
    indent.imprimer('Niveau 1')
    with indent:
        indent.imprimer('Niveau 2')
        with indent:
            indent.imprimer('Niveau 3')
    indent.imprimer('Encore niveau 1')

```

```shell
$ python contexte.py
    Niveau 1
        Niveau 2
            Niveau 3
    Encore niveau 1

```

Dans cet exemple, `Indentation` maintient un état `niveau` qui est incrémenté à chaque niveau de contexte imbriqué et décrémenté lors de la sortie de chaque niveau. Cela permet d'adapter le comportement du manager réentrant en fonction du niveau de contexte actuel. En sortie, on voit la structure qui reflète la profondeur du contexte grâce à l'indentation.

## Pour les curieux

Je vous renvoie vers la [documentation officielle du module contextlib](https://docs.python.org/fr/3/library/contextlib.html?ref=sfeir.dev). Cette page revient sur le décorateur que l'on a rapidement évoqué mais parle aussi des gestionnaires de contexte asynchrones, des gestionnaires réutilisables comme le `Lock` ou encore du gestionnaire `ExitStack`.

---

1. Si vous avez bien suivi, `open` retourne un objet qui est un flux, mais qui est aussi un context manager qu'on peut utiliser avec `with`. Pour en savoir plus, voici les liens vers la documentation de la [fonction open](https://docs.python.org/fr/3/library/functions.html?ref=sfeir.dev#open) et de l'[objet IOBase](https://docs.python.org/fr/3/library/io.html?ref=sfeir.dev#io.IOBase). [↩︎](#fnref1)