Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Licence CC BY-NC-ND Thierry Parmentelat & Arnaud Legout Inria - UCA

Complément - niveau intermédiaire

On a vu jusqu’ici dans la vidéo comment écrire un context manager; on a vu notamment qu’il était bon pour la méthode __exit__() de retourner False, de façon à ce que l’exception soit propagée à l’instruction with:

import time

class Timer1:
    def __enter__(self):
        print("Entering Timer1")
        self.start = time.time()
        return self
    
    # en règle générale on se contente de propager l'exception 
    # à l'instruction with englobante
    def __exit__(self, *args):
        print(f"Total duration {time.time()-self.start:2f}")

        # et pour cela il suffit que __exit__ retourne False
        return False

Ainsi si le corps de l’instruction lève une exception, celle-ci est propagée :

import time
try:
    with Timer1():
        time.sleep(0.5)
        1/0
except Exception as exc:
    # on va bien recevoir cette exception
    print(f"OOPS -> {type(exc)}")
Entering Timer1
Total duration 0.500078
OOPS -> <class 'ZeroDivisionError'>

À la toute première itération de la boucle, on fait une division par 0 qui lève l’exception ZeroDivisionError, qui passe bien à l’appelant.

Il est important, lorsqu’on conçoit un context manager, de bien propager les exceptions qui ne sont pas liées au fonctionnement attendu du context manager. Par exemple un objet de type fichier va par exemple devoir attraper les exceptions liées à la fin du fichier, mais doit par contre laisser passer une exception comme ZeroDivisionError.

Les paramètres de __exit__

Si on a besoin de filtrer entre les exceptions - c’est-à-dire en laisser passer certaines et pas d’autres - il nous faut quelque chose de plus pour pouvoir faire le tri. Comme vous pouvez le retrouver ici, la méthode __exit__ reçoit trois arguments :

def __exit__(self, exc_type, exc_value, traceback):

Pour illustrer cela, écrivons une nouvelle version de Timer qui filtre, disons, l’exception ZeroDivisionError que je choisis au hasard, c’est uniquement pour illustrer le mécanisme.

# une deuxième version de Timer
# qui propage toutes les exceptions sauf 'ZeroDivisionError'

class Timer2:
    def __enter__(self):
        print("Entering Timer1")
        self.start = time.time()
        # rappel : le retour de __enter__ est ce qui est passé
        # à la clause `as` du `with`
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            # pas d'exception levée dans le corps du 'with'
            print(f"Total duration {time.time()-self.start:2f}")
            # dans ce cas la valeur de retour n'est pas utilisée
        else:
            # il y a eu une exception de type 'exc_type'
            if exc_type in (ZeroDivisionError,) :
                print("on étouffe")
                # on peut l'étouffer en retournant True
                return True
            else:
                print(f"OOPS : on propage l'exception "
                      f"{exc_type} - {exc_value}")
                # et pour ça il suffit... de ne rien faire du tout
                # ce qui renverra None
# commençons avec un code sans souci
try:
    with Timer2():
        time.sleep(0.5)
except Exception as e:
    # on va bien recevoir cette exception
    print(f"OOPS -> {type(e)}")
Entering Timer1
Total duration 0.500088
# avec une exception filtrée
try:
    with Timer2():
        time.sleep(0.5)
        1/0
except Exception as e:
    # on va bien recevoir cette exception
    print(f"OOPS -> {type(e)}")
Entering Timer1
on étouffe
# avec une autre exception 
try:
    with Timer2():
        time.sleep(0.5)
        raise OSError()
except Exception as e:
    # on va bien recevoir cette exception
    print(f"OOPS -> {type(e)}")
Entering Timer1
OOPS : on propage l'exception <class 'OSError'> - 
OOPS -> <class 'OSError'>

La bibliothèque contextlib

Je vous signale aussi la bibliothèque contextlib qui offre quelques utilitaires pour se définir un contextmanager.

Notamment, elle permet d’implémenter un context manager sous une forme compacte à l’aide d’une fonction génératrice - et du décorateur contextmanager:

from contextlib import contextmanager
The history saving thread hit an unexpected error (OperationalError('attempt to write a readonly database')).History will not be written to the database.
# l'objet compact_timer est un context manager !
@contextmanager
def compact_timer(message):
    start = time.time()
    yield
    print(f"{message}: duration = {time.time() - start}")
with compact_timer("Squares sum"):
    print(sum(x**2 for x in range(10**5)))
333328333350000
Squares sum: duration = 0.019451618194580078

Un peu comme on peut implémenter un itérateur à partir d’une fonction génératrice qui fait (n’importe quel nombre de) yield, ici on implémente un context manager compact sous la forme d’une fonction génératrice.

Comme vous l’avez sans doute deviné sur la base de cet exemple, il faut que la fonction fasse exactement un yield: ce qui se passe avant le yield est du ressort de __enter__, et la fin est du ressort de __exit__().

Bien entendu on n’a pas la même puissance d’expression avec cette méthode par rapport à une vraie classe, mais cela permet de créer des context managers avec le minimum de code.