Les context managers et le mot clé with en Python


Le mot clé with est utilisé comme dans aucun autre langage en Python. Au premier abord mystérieux, il agit en fait comme les décorateurs en permettant d’exécuter du code automatiquement avant et après un autre code. Mais à l’image des décorateurs, tout ce qu’il fait pourrait être écrit à la main sans utiliser le mot clé with. Utiliser with est une question de style.

Supposons que vous vouliez afficher quelque chose avant un bout de code, et après un bout de code, même si celui-ci rate. Vous feriez quelque chose comme ça:

def truc():
    print "machin"
 
print "Avant"
try:
    truc()
finally:
    print "Après"

Et ça va afficher:

Avant
machin
Après

Et avec:

def truc():
    print "machin"
    raise Exception('Fail !')

‘Après’ sera quand même affiché. Ça plantera, mais la dernière action sera toujours faite.

Si vous le faites souvent, vous voudrez factoriser du code. Un des moyens de le faire est d’utiliser les context managers.

Créer son propre context manager

Un context manager est une classe ordinaire en Python. Sa seule spécificité est de déclarer une méthode __enter__() et une méthode __exit__(). Ces méthodes sont des méthodes ordinaires, leur nom spécial est juste là par convention, et en les nommant ainsi on s’assure qu’elles seront détectées et utilisées automatiquement.

Notre code là haut peut donc se réécrire ainsi:

class MonSuperContextManager(object):
    def __enter__(self):
        print "Avant"
    def __exit__(self, type, value, traceback):
        # faites pas attention aux paramètres, ce sont toutes les infos
        # automatiquement passées à __exit__ et qui servent pour inspecter
        # une éventuelle exception
        print "Après"
 
with MonSuperContextManager():
    truc()

L’avantage de with est multiple:

  • Il permet de visualiser très précisément où on entre dans l’action et où on en sort (c’est un seul block)
  • Il permet de réutiliser les actions faite à l’entrée et à la sortie de l’action.
  • Même si une exception est levée, l’action de sortie sera exécutée juste avant le plantage. __exit__ est en effet garantie d’être appelée quoiqu’il arrive. Bon, évidement, si il y a une coupure de courant…

En gros, créer un context manager, c’est faire un raccourci lisible pour try/finally. Point.

Un exemple utile de context manager

Supposons que vous ayez beaucoup de travail à faire dans plein de dossiers. Vous voulez vous assurer que vous allez dans le dossier de travail, puis que vous retournez au dossier initial à chaque fois.

import os
 
class Cd(objet):
    def __init__(dirname):
        self.dirname = dirname
    def __enter__(self):
        self.curdir = os.getcwd()
        os.chdir(self.dirname)
    def __exit__(self, type, value, traceback):
        os.chdir(self.curdir)

On l’utilise comme ça:

# ici on est dans /home/moi
 
with Cd('/'):
 
    # faire un truc dans /
 
    with Cd('/opt'):
 
        # faire un truc dans /opt
 
    # ici on est dans /
 
# ici on est dans /home/moi

C’est d’ailleurs ce que fait fabric.

Le mot clé as

Tout ce qu’on retourne dans __enter__ peut être récupéré grâce au mot clé as. Imaginons un context manager qui permette d’ouvrir un fichier et de le fermer automatiquement:

class OpenFile(objet):
    def __init__(filename, mode='r'):
        self.filename = filename
        self.mode = mode
    def __enter__(self):
        self.file = open(self.filename, self.mode)
        # ici on retourne l'objet fichier, il sera accessible avec "as"
        return self.file
    def __exit__(self, type, value, traceback):
        self.file.close()

On l’utilise comme ceci:

with OpenFile('/etc/fstab') as f:
    for line in f:
        print line

f va contenir ici l’objet fichier, car nous l’avons retourné dans __enter__. A la fin du bloc with, le fichier sera fermé automatiquement.

Et devinez quoi, Python possède déjà un context manager qui fait ça:.

with open(vot_fichier_msieu_dames) as f:
   # faire un truc

Context managers sous forme de fonctions

Faire les choses sous forme de classes, c’est pratique quand on a beaucoup de logique à encapsuler. Mais la plupart des context managers sont très simples. Pour cette raison, Python vient avec plein d’outils pour se simplifier la vie avec with dans un module judicieusement nommé contextlib.

Pour l’utiliser, il faut avoir des notions sur les décorateurs, et le mot clé yield. Si ce n’est pas votre cas, restez sur la version sous forme de classe :-)

Supposons que l’on veuille recréer le context manager open:

from contextlib import contextmanager
 
@contextmanager
def open(filename, mode):
    try:
        f = open(filename, mode)
        yield f
    finally:
        f.close()

Bon, c’est simplifié, hein, le vrai est plus robuste que ça.

Comment ça marche ?

D’abord, on utilise le décorateur @contextmanager pour dire à Python que la fonction sera un context manager.

Ensuite, on fait un try/finally (il est pas automatique comme avec __enter__ et __exit__).

yield sépare le code en deux: tout ce qui est avant est l’équivalent de __enter__, tout ce qui est après est l’équivalent de __exit__. Ce qui est “yieldé” est ce que l’on récupère avec le mot clé as.

Context manager et décorateur, le shampoing deux en un

Ces deux fonctionnalités se ressemblent beaucoup: elles permettent toutes les deux de lancer du code automatiquement avant et après un code tiers. La seule différence est que le context manager le fait à la demande, alors que le décorateur s’applique à la définition d’une fonction.

Quand on sait comment ils marchent, il est facile de faire un context manager utilisable également en tant que décorateur.

from functools import wraps
 
class ContextDecorator(object):
    # __call__ est une méthode magique appelée quand on utilise () sur un objet
    def __call__(self, f):
        # bon, cette partie là suppose que vous savez comment marche un
        # décorateur, si c'est pas le cas, retournez lire l'article sur S&M
        # linké dans le premier paragraphe
        @wraps(f)
        def decorated(*args, **kwds):
            # notez le with appelé sur soi-même, c'est y pas mignon !
            with self:
                return f(*args, **kwds)
        return decorated

Et voilà, il suffit d’hériter de ça, et on a un décorateur + context manager. Par exemple, si on veut timer un truc:

import datetime
 
class TimeIt(ContextDecorator):
 
    def __enter__(self):
        self.start = datetime.datetime.now()
        print self.start
 
    def __exit__(self, type, value, traceback):
        print (datetime.datetime.now() -self.start).total_seconds()

Timer juste un appel:

def foo():
    # faire un truc
 
with TimeIt():
    foo()

Timer tous les appels:

@TimeIt()
def foo():
   # faire un truc

Notez que ContextDecorator est présent par défaut dans le module contextlib sous Python 3.2.

20 thoughts on “Les context managers et le mot clé with en Python

  • fero14041

    C’est beau, merci! Une remarque ou question à propos (mais (pré)caution: yeux dans le cirage): dans la section “Un exemple utile de context manager“, j’aurais naïvement référencé la variable curdir comme attribut de l’instance, non?

    class Cd(objet):
    #… méthodes précédentes …

    def __exit__(self, type, value, traceback):
    os.chdir(self.curdir)

  • Lujeni

    Encore un très beau article sur des concepts de python peu souvent utilisé ( à mon sens ) ! Vivement les prochains articles :) ! Enjoy

  • Zariko

    Super article !

    Petite typo sur le ContextDecorator: @functools.wraps => @wraps

  • Sam Post author

    Merci zariko.

    Maintenant il faut que je trouve un moyen de placer “la fin des zarikos” dans un blog post.

  • Sam Post author

    Au passage, j’ai eu la question:

    “Mais en quoi c’est utile le context manager pour open ?”

    try:
        with open("fichier", "r") as f:
            ligne = f.readline()
    except IOError:
        print "Erreur"

    “Est a peine plus court que:”

    try:
        f = open("fichier", "r")
        ligne = f.readline()
    except IOError:
        print "Erreur"
    finally:
        f.close()

    Alors je note la réponse ici:

    Ces deux codes ci-dessus ne sont PAS équivalents.

    En effet, l’équivalent sans le context manager est:

    try:
        f = open("fichier", "r")
        try:
            ligne = f.readline()
        finally:
            f.close()
    except IOError:
        print "Erreur"

    Avec un double try imbriqué.

    En effet, il peut y avoir une erreur à l’ouverture du fichier, et dans ce cas la clause finally va planter, car f n’existera pas !

    Utiliser with permet de s’affranchir de ce genre de petits détails.

  • Etienne

    Vraiment chouette tout ça. Ce qui est bien, c’est que les contextmanager peuvent être utilisé sur des bouts de code de taille arbitraire.

    Je pense à ton TimeIt par exemple, qui pourrait logger le temps d’exécution de portions de code critique (genre query dans db qui grossit rapidement), et/ou pourrait effectuer un action si ce temps dépasse un seuil donné.

    Sympa, sympa

  • Lujeni

    Hello, dans l’exemple du code mixant Decorator&Context il manque un t sur la ligne self.start = datetime.dateime.now(). L’utilisation du decorator TimeIt semble lèver une exception TypeError: object.__new__() takes no parameters.

  • hamzahik

    with OpenFile('/etc/fstab') as f:
    for line in f:
    print line #au lieu de "print f" je pense

  • bato-san

    @Lujeni

    @TimeIt
    def ma_fonction():
      blabla

    correspond a :

    def ma_fonction():
       blabla
       ma_fonction = TimeIt(ma_fonction)

    or ici TimeIt est la ref d une classe et non pas d une instance de classe , donc ” TimeIt(ma_fonction) ” appelle la méthode __new__() de TimeIt ( et non pas __call__() ) et lui passe en paramètre une fonction , ce qui n est pas apprecié de la part du compilateur X)
    Pour appeler la méthode __call__() (et qu accessoirement ca marche ;v ), il faut donc écrire :

    def ma_fonction():
      blabla
      ma_fonction = TimeIt()(ma_fonction)

    soit:

    @TimeIt()
    def ma_fonction():
      blabla

    En espérant avoir aidé :)

  • Sam Post author

    J’ai corrigé toutes les coquilles. Merci pour votre vigilance, les articles gagnent vraiment en qualité quand vous corrigez mes conneries :)

  • Sébastien

    Je cherchais à générer un fichier XML. L’idée d’utiliser la notion de context manager me trottait. En Googlant un peu j’ai trouvé cette excellente bibliothèque qui mérite d’être connue : http://www.yattag.org/ https://github.com/leforestier/yattag

     
    from yattag import Doc, indent
     
    doc, tag, text = Doc().tagtext()
     
    with tag('persons'):
        with tag('person', id = '0'):
            with tag('name'):
                text('Pierre')
        with tag('person', id = '1'):
            with tag('name'):
                text('Paul')
        with tag('person', id = '2'):
            with tag('name'):
                text('Jacques')
     
    result = indent(
        doc.getvalue(),
        indentation = ' '*4,
        newline = 'rn'
    )
     
    print(result)

    Ce bout de code permet de générer:

     
     
     
     
     
    Pierre
     
     
     
     
     
    Paul
     
     
     
     
     
    Jacques
  • ast

    “tain le décorateur/context manager 2 en 1 pas simple ! Pas sur que j’arrive à le réécrire sans regarder l’article.

  • Nob

    Hello,

    Je poste le commentaire ici mais il s’applique à tout votre blog (et les articles que j’ai lu.)

    Merci pour l’effort et la qualité de vos articles,j’ai pu comprendre typiquement le fonctionnement des descripteurs ainsi que des contexts manager rapidement (alors qu’avec le document de référence j’y serais encore.) !

  • Antoine

    Merci pour vos super articles, cela m’a permis d’avancer à vitesse V en python (et passer de 0 ligne à +4000 lignes de code en 2 mois).

    Petites coquilles dans la page “context managers et with”:

    class Cd(objet): à rectifier en object

    et dans init, il manque self

    class OpenFile(objet): à rectifier en object

    et dans init, il manque self

  • Antoine

    Tout est vraiment bien, une seule petite remarque:

    dans la rubrique “Context managers sous forme de fonctions”, le nom open conduit à une erreur (du moins en python 3.5.2). A remplacer par:

    @contextmanager

    def opened(filename, mode):

    ….

    D’autre part, cela ne reproduit pas tous les comportements du open natif.

    with opened( ‘/etc/fstab’, ‘r’ ) as f:

    …. for line in f: print( line, end=” ) # c’est Ok

    Les deux blocks suivants, qui marchent avec open natif, donnent des erreurs avec opended

    f = opened(( ‘/etc/fstab’, ‘r’ )

    with f:

    …. for line in f: print( line, end=” ) # TypeError: ‘_GeneratorContextManager’ object is not iterable

    et

    f = opened(( ‘/etc/fstab’, ‘r’ )

    for line in f: print( line, end=” ) # TypeError: ‘_GeneratorContextManager’ object is not iterable

    f.close

    Par quel mystère le open natif accepte-t’il ces trois syntaxes? J’ai cherché, cherché … et … Big silence …

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.