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.
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)
Yes, bien vu. Typo corrigée.
Encore un très beau article sur des concepts de python peu souvent utilisé ( à mon sens ) ! Vivement les prochains articles :) ! Enjoy
Super article !
Petite typo sur le ContextDecorator: @functools.wraps => @wraps
Merci zariko.
Maintenant il faut que je trouve un moyen de placer “la fin des zarikos” dans un blog post.
Très bon article ! Merci les gars !
Au passage, j’ai eu la question:
“Mais en quoi c’est utile le context manager pour open ?”
“Est a peine plus court que:”
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:
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, carf
n’existera pas !Utiliser
with
permet de s’affranchir de ce genre de petits détails.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
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.with OpenFile('/etc/fstab') as f:
for line in f:
print line #au lieu de "print f" je pense
@Lujeni
correspond a :
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 :
soit:
En espérant avoir aidé :)
J’ai corrigé toutes les coquilles. Merci pour votre vigilance, les articles gagnent vraiment en qualité quand vous corrigez mes conneries :)
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
Ce bout de code permet de générer:
“tain le décorateur/context manager 2 en 1 pas simple ! Pas sur que j’arrive à le réécrire sans regarder l’article.
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.) !
:)
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
Merci.
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 …
T’as oublié de passer le self dans tes constructeurs.