Skip to article frontmatterSkip to article content

l’instruction with .. as s’utilise exclusivement avec des objets qui sont des context managers dans ce notebook on va approfondir cette notion

déjà rencontré

où trouve-t-on des context managers ?
eh bien par exemple, un objet fichier est un context manager

nous avons déjà vu qu’on peut utiliser un objet fichier - tel que renvoyé par open() - comme ceci

with open("../data/une-charogne.txt") as mon_fichier:
    for l in mon_fichier:
        print(l)

ce qui garantit que le fichier OS est fermé automatiquement à la fin du with quoi qu’il arrive - qu’il se produise des exceptions ou non à l’intérieur du with

un peu plus formellement

lorsqu’on exécute un with qui a la forme générale suivante

with expression [as variable]:
	with-block
    ...

c’est-à-dire:

protocole context manager

l’objet résultat de l’expression doit avoir deux méthodes __enter__ et __exit__

exemple avec relance d’exception

# a context manager class that will raise the exceptions

class Raises():
    
    def __enter__(self):
        print("in enter()")
        return self

    def __exit__(self, *args):
        print(f"in exit: args={args}")
        return False   # relance l'exception
# because of that we still need to use a try .. except

import traceback

try:
    # we do not need the result, so no need for an 'as' clause
    with Raises():
        # purposefully triggering an exception
        1 / 0
except:
    print("OOPS - we catch the exception")
    traceback.print_exc()
in enter()
in exit: args=(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero'), <traceback object at 0x7f4b48f0dc00>)
OOPS - we catch the exception
Traceback (most recent call last):
  File "/tmp/ipykernel_2586/22239959.py", line 9, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero
# note that the exit phase is always called
with Raises():
    # no exception here
    pass
in enter()
in exit: args=(None, None, None)

exemple sans relance d’exception

# a context manager class that will block the exceptions

class Blocks():

    def __enter__(self):
        print("in enter()")
        return self

    def __exit__(self, *args):
        print(f"in __exit__: args={args}")
        return True  # this is how we say we want to block this exception
                     # of course in real code we could decide this depending
                     # on the exception itself
with Blocks():
    1 / 0
print("life goes on")
in enter()
in __exit__: args=(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero'), <traceback object at 0x7f4b48f0e700>)
life goes on

utilisations classiques

le plus souvent on va utiliser cette notion pour toutes les opérations un peu transactionnelles, où on doit proprement refermer les choses, ce qui correspond en pratique:

typiquement, un handle d’accès à une base de données sera souvent exposé comme un context manager
pareillement, les protocoles réseau offriront souvent une API de context manager, à la différence toutefois que dans ce cas ils seront asynchrones; mais là c’est encore un autre dossier complètement ...

contextlib

sachez enfin que la librairie contextlib expose un décorateur qui simplifie l’implémentation d’un context manager
grâce à ce décorateur on s’économise la définition d’une classe, et obtenir un context manager en décorant un générateur, comme par exemple

import contextlib

@contextlib.contextmanager
def custom_database_connector(url):
    session = ...   # how to connect to the DB
    yield session
    session.close()

with custom_database_connector("https://database.example.com/") as session:
    ... # do the DB black magic using session

utilisation originale

citons enfin un usage moins académique, mais intéressant aussi
on peut utiliser un context manager pour exécuter du code avant et après une opération
par exemple, pour mesurer le temps d’exécution, même en cas d’exception
dans cet usage, c’est proche d’un décorateur

import time

class Timer():    

    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        self.end = time.time()
        print(f"durée d'exécution = {self.end - self.start:2f}")
        return False
try:
    with Timer() as t:
        # do some stuff 
        [x ** 3 for x in range(100000)]
        # trigger an exception
        1 / 0
except:
    traceback.print_exc()
durée d'exécution = 0.056149
Traceback (most recent call last):
  File "/tmp/ipykernel_2586/2676233390.py", line 6, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero