Rendre les objets compatibles avec l’instruction with en Python

Vous voulez que vos objets supportent le protocole de gestion de contexte (l’instruction with). Afin de rendre un objet compatible avec l’instruction with, vous devez implémenter les méthodes __enter__() et __exit__(). Par exemple, considérez la classe suivante, qui fournit une connexion réseau:

from socket import socket, AF_INET, SOCK_STREAM

class _Connexion:
    def __init__(self, adresse, famille=AF_INET, type=SOCK_STREAM):
        self.adresse = adresse
        self.famille = famille
        self.type = type
        self.sock = None

    def __enter__(self):
        if self.sock is not None:
            raise RuntimeError('Déja connecté')
        self.sock = socket(self.famille, self.type)
        self.sock.connect(self.adresse)
        return self.sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.sock.close()
        self.sock = None

La caractéristique clé de cette classe est qu’elle représente une connexion réseau, mais qu’elle ne fait rien au départ (par exemple, elle n’établit pas de connexion).

Au lieu de cela, la connexion est établie et fermée à l’aide de l’instruction with (essentiellement sur demande). Par exemple:

from functools import partial

conn = _Connexion(('www.python.org', 80))
# Connexion fermée
with conn as s:
    # conn.__enter__() s'exécute : connexion ouverte
    s.send(b'GET /index.html HTTP/1.0\r\n')
    s.send(b'Host: www.python.org\r\n')
    s.send(b'\r\n')
    resp = b''.join(iter(partial(s.recv, 8192), b''))
    # conn.__exit__()  s'exécute : connexion fermée

Le principe fondamental derrière la rédaction d’un gestionnaire de contexte est que vous écrivez du code qui est destiné à entourer un bloc d’instructions tel que défini par l’utilisation de l’instruction with.

Lorsque l’instruction with est rencontrée pour la première fois, la méthode __enter__() est déclenchée. La valeur de retour de __enter__() (le cas échéant) est placée dans la variable indiquée avec le qualificateur as.

Ensuite, les instructions dans le corps de l’instruction with s’exécutent. Enfin, la méthode __exit__() est déclenchée pour nettoyer.

Ce flux de contrôle se produit indépendamment de ce qui se passe dans le corps de l’instruction with, y compris s’il y a des exceptions.

En fait, les trois arguments de la méthode __exit__() contiennent le type d’exception, la valeur et la traceback des exceptions en attente (le cas échéant).

La méthode __exit__() peut choisir d’utiliser l’information d’exception d’une manière ou d’une autre ou de l’ignorer en ne faisant rien et en retournant None comme résultat.

Si __exit__() renvoie True, l’exception est effacée comme si rien ne s’était passé et le programme continue à exécuter les instructions immédiatement après le bloc with.

Un aspect subtil de ce code est de savoir si la classe _Connexion permet ou non l’utilisation imbriquée de la connexion avec plusieurs instructions.

Comme le montre l’illustration, une seule connexion de socket à la fois est autorisée, et une exception est levée si une tentative répétée de l’instruction with est essayée quand une socket est déjà en service.

Vous pouvez contourner cette limitation avec une implémentation légèrement différente, comme montré ici:

from socket import socket, AF_INET, SOCK_STREAM

class _Connexion:
    def __init__(self, adresse, famille=AF_INET, type=SOCK_STREAM):
        self.adresse = adresse
        self.famille = AF_INET
        self.type = SOCK_STREAM
        self.connexions = []

    def __enter__(self):
        sock = socket(self.famille, self.type)
        sock.connect(self.adresse)
        self.connexions.append(sock)
        return sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.connexions.pop().close()

# Exemple
from functools import partial

conn = _Connexion(('www.python.org', 80))
with conn as s1:
     ...
     with conn as s2:
          ...
          # s1 et s2 sont des sockets independentes

Dans cette deuxième version, la classe _Connexion sert en quelque sorte d’usine pour les connexions. En interne, une liste est utilisée pour stocker une pile. Chaque fois que __enter__() s’exécute, il établit une nouvelle connexion et l’ajoute à la pile.

La méthode __exit__() fait tout simplement sauter la dernière connexion de la pile et la ferme. C’est subtil, mais cela permet de créer plusieurs connexions à la fois avec des instructions with imbriquées, comme indiqué.

Les gestionnaires de contexte sont le plus souvent utilisés dans les programmes qui ont besoin de gérer des ressources telles que les fichiers, les connexions réseau et les verrous.

Une partie essentielle de ces ressources est qu’elles doivent être explicitement fermées ou libérées pour fonctionner correctement.

Par exemple, si vous faites l’acquisition d’une serrure, vous devez vous assurer que vous la libérez, sinon vous risquez une impasse.

En implémentant __enter__(), __exit__(), et en utilisant l’instruction with, il est beaucoup plus facile d’éviter de tels problèmes, puisque le code de nettoyage dans la méthode __exit__() est garanti pour fonctionner quoi qu’il arrive.

Une formulation alternative des gestionnaires de contexte se trouve dans le module contextmanager.

LAISSER UN COMMENTAIRE

Please enter your comment!
Please enter your name here