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
...bien sûr on commence par évaluer l’expression
puis on applique sur l’objet résutat le protocole de context manager
c’est-à-dire:
protocole context manager¶
l’objet résultat de l’expression doit avoir deux méthodes __enter__ et __exit__
entrée dans le
with:à l’entrée du contexte,
__enter__(self)est exécuté;le retour de
__enter__est assigné à la variable mentionnée dans le.. as var
à ce stade le code with-block est executé
sortie sans exception
__exit__(self, None, None, None)est appelée, toujours sur le context manager
sortie en cas d’exception:
__exit__(self, exc_type, exc_value, exc_traceback)est appelé avecexc_type,exc_value,exc_tracebacksont les type, valeur et traceback (pile) de l’exceptionle retour de
__exit__est utilisé:
siFalse➔ l’exception est relancée
siTrue➔ l’exception est supprimée (étouffée)
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
passin 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 itselfwith 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:
à l’allocation / libération de ressources
au temps long en terme d’OS, c’est à dire par exemple les accès réseau
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 sessionutilisation 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 Falsetry:
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