XXIII. Le « context manager »▲
XXIII-1. Préambule▲
Le « context manager » est un mécanisme qui garantit un nettoyage automatique dans le cas où le code quitte le bloc de travail de façon impromptue.
Prenons l’exemple d’un fichier contenant des nombres (un nombre par ligne) et d’un algorithme affichant chaque nombre inversé.
Dans le cas où le fichier contient une ligne avec le nombre 0
, une exception ZeroDivisionError
est levée et remonte la chaîne des appels jusqu’à, si elle n’est pas récupérée, sortir du programme. Mais l'instruction de fermeture du fichier n’est alors jamais exécutée.
Dans ce cas précis, certains pourront ne pas s’en préoccuper, car le fichier est ouvert en lecture et de toute façon, il est quand même fermé par l’OS. Mais, d’une part, il s’agit d’une attitude peu professionnelle, et d’autre part, dans d’autres circonstances, cela peut-être bien plus catastrophique. En cas d’écriture par exemple, l’OS ne garantit pas la finalisation des écritures et tout n’est alors pas forcément écrit.
Par ailleurs, si l’exception ne remonte pas jusqu’à l’OS, mais qu’elle est récupérée dans une fonction en amont, la fonction n’a pas forcément connaissance qu’une ressource (fichier ouvert) avait été allouée…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
def
calcul
(
):
res=
0
fp=
open(
"fic"
, "r"
)
for
lig in
fp: res+=
1
/
int(
lig)
fp.close
(
)
return
res
# calcul()
def
traitement
(
):
try
:
print
(
calcul
(
))
except
ArithmeticError
as
e:
print
(
"Le calcul n’a pas pu se faire…
%s
"
%
e)
# Et le fichier est resté ouvert !!!
# try
# traitement()
XXIII-2. Utilisation▲
Une première solution sera de protéger le traitement du fichier par un bloc try
pour pouvoir le fermer dans la clause finally
et de protéger aussi l’appel à la fonction par un autre bloc try
pour pouvoir gérer l’exception qui en remonte.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
def
calcul
(
):
res=
0
try
:
fp=
open(
"fic"
, "r"
)
for
lig in
fp: res+=
1
/
int(
lig)
finally
:
fp.close
(
)
# try
return
res
# calcul()
def
traitement
(
):
try
:
print
(
calcul
(
))
except
ArithmeticError
as
e:
print
(
"Le calcul n’a pas pu se faire…
%s
"
%
e)
# Mais le fichier est au moins fermé !!!
# try
# traitement()
Cela fonctionne, mais donne un code assez lourd et maladroit. Deux try
pour un seul travail et un autre développeur en collaboration, mais qui n’a pas forcément connaissance des détails, pourrait se demander pourquoi ce try
sans except
.
La seconde solution sera d’utiliser un context manager. On le met en place par la création d’un bloc with
.
Le principe de sa syntaxe est de remplacer une instruction var=
ressource
(
) par with
ressource
(
) as
var. Et comme il s’agit d’un bloc, il ne faut pas oublier les deux‑points terminant l’instruction.
Ensuite, on place dans le bloc toutes les instructions qui ont besoin d’accéder à la ressource. Et quoi qu’il se passe, Python garantit la libération de la ressource quand il quittera le bloc, et ce, quelle que soit la façon de le quitter.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
def
calcul
(
):
res=
0
with
open (
"fic"
, "r"
) as
fp:
for
lig in
fp: res+=
1
/
int(
lig)
# with
# Ici, le fichier est automatiquement fermé – Même pas besoin de le demander
return
res
# calcul()
def
traitement
(
):
try
:
print
(
calcul
(
))
except
ArithmeticError
as
e:
print
(
"Le calcul n’a pas pu se faire…
%s
"
%
e)
# Mais le fichier est quand même fermé !
# try
# traitement()
Et afin d’éviter la démultiplication des blocs (donc des tabulations avec les décalages de code qui en résultent) dans le cas de plusieurs ressources à suivre…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
with
open (
"fic1"
, "r"
) as
fp1:
with
open(
"fic2"
, "r"
) as
fp2:
print
(
"ouvert
%s
%s
"
%
(
"fp1"
if
not
fp1.closed else
""
,
"fp2"
if
not
fp2.closed else
""
,
)
)
# with
# Ici, le second fichier est automatiquement fermé
print
(
"ouvert
%s
%s
"
%
(
"fp1"
if
not
fp1.closed else
""
,
"fp2"
if
not
fp2.closed else
""
,
)
)
# with
# Ici, le premier fichier est automatiquement fermé
print
(
"ouvert
%s
%s
"
%
(
"fp1"
if
not
fp1.closed else
""
,
"fp2"
if
not
fp2.closed else
""
,
)
)
… il est possible de chaîner plusieurs with
en utilisant la virgule.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
with
open (
"fic1"
, "r"
) as
fp1, open(
"fic2"
, "r"
) as
fp2:
print
(
"ouvert
%s
%s
"
%
(
"fp1"
if
not
fp1.closed else
""
,
"fp2"
if
not
fp2.closed else
""
,
)
)
# with
# Ici, les deux fichiers sont automatiquement fermés
print
(
"ouvert
%s
%s
"
%
(
"fp1"
if
not
fp1.closed else
""
,
"fp2"
if
not
fp2.closed else
""
,
)
)
XXIII-3. Création de son propre « context manager »▲
Il est bien évidemment possible de créer son propre context manager. Cela se fait par le biais d’un objet dans lequel le constructeur __init__
(
) devra être prévu pour recevoir tous les paramètres habituellement passés lors de l’appel à ressource
(
) et auquel on rajoute deux méthodes spécifiques :
la méthode
__enter__
(
) : cette méthode est appelée lors de l’entrée dans le blocwith
;la méthode
__exit__
(
) : cette méthode est appelée lorsque le blocwith
se termine.
Exemple : une réécriture simplifiée de la fonction open(
)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
class
myOpen:
# Constructeur
def
__init__
(
self, name, mode=
"r"
):
print
(
"Création
%s
,
%s
"
%
(
name, mode))
# Il faut mémoriser le nom et le mode, car l’objet en aura besoin pour l’ouvrir
(
self.__name, self.__mode)=(
name, mode)
# __init__()
# Quand le context manager demande la ressource
def
__enter__
(
self):
print
(
"Ouverture
%s
,
%s
"
%
(
self.__name, self.__mode))
# Il faut mémoriser le fichier ouvert, car l’objet en aura besoin pour le fermer
self.__fp=
open(
self.__name, self.__mode)
# Ici on retourne le fichier ouvert. Il sera accessible avec "as"
return
self.__fp
# __enter__()
# Quand le context manager quitte le bloc
def
__exit__
(
self, tp, e, traceback):
print
(
"Fermeture
%s
,
%s
- tp=
%s
, e=
%s
, traceback=
%s
"
%
(
self.__name, self.__mode,
tp, e, traceback,
)
)
# On ferme le fichier, opération qui était le but premier de ce context manager
self.__fp.close
(
)
# __exit__()
# class myOpen
Et son utilisation :
2.
with
myOpen
(
"/etc/passwd"
) as
fp:
for
lig in
fp: print
(
lig)
Les paramètres tp, e et traceback sont automatiquement remplis quand le bloc with
est interrompu par une exception. tp récupère le type de l’exception, e récupère le message d’erreur associé à l’exception et traceback récupère un objet traceback permettant de retracer manuellement le contenu de la pile des appels.
2.
with
myOpen
(
"/etc/passwd"
) as
fp:
raise
IOError
(
"erreur bye bye"
)
Autre exemple : revenir automatiquement dans le répertoire d’origine après s’être déplacé dans l’arborescence.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
import
os
class
myCD:
def
__init__
(
self, rep=
os.getenv
(
"HOME"
)):
# On mémorise le dossier demandé pour pouvoir y aller plus tard
self.__rep=
rep
# __init__()
def
__enter__
(
self):
# On mémorise le dossier courant pour pouvoir y revenir à la fin
self.__cwd=
os.getcwd
(
)
# Et on se déplace dans le dossier demandé
os.chdir
(
self.__rep)
# __enter__()
def
__exit__
(
self, tp, e, traceback):
# On revient dans le dossier d'origine
os.chdir
(
self.__cwd)
# __exit__()
# class myCD
# On se déplace dans un dossier initial pour les tests
os.chdir
(
"/"
)
print
(
"dir0=
%s
"
%
os.getcwd
(
))
with
myCD
(
"/tmp"
):
print
(
"dir1=
%s
"
%
os.getcwd
(
))
with
myCD
(
"/var"
):
print
(
"dir2=
%s
"
%
os.getcwd
(
))
with
myCD
(
):
print
(
"dir3=
%s
"
%
os.getcwd
(
))
# with
print
(
"dir2=
%s
"
%
os.getcwd
(
))
# with
print
(
"dir1=
%s
"
%
os.getcwd
(
))
# with
print
(
"dir0=
%s
"
%
os.getcwd
(
))