IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

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é.

 
Sélectionnez
1.
2.
3.
fp=open(fic, "r")
for lig in fp: print(1/int(lig))
fp.close()

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…

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 bloc with ;

  • la méthode __exit__() : cette méthode est appelée lorsque le bloc with se termine.

Exemple : une réécriture simplifiée de la fonction open()

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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())

précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.