Outils personnels
Vous êtes ici : Accueil Python Quoi de neuf dans Python 2.5 PEP 343: L'instruction "with"

PEP 343: L'instruction "with"

Par Benjamin Poulain - Dernière modification 18/04/2008 21:03
Contributeurs : Bozo_le_clown
Python License

Python 2.5 introduit une nouvelle méthode pour exécuter proprement du code en étant sûr qu'une fonction de nettoyage sera appelée en cas de problème : l'instruction with. L'instruction with rend facile certaines tâches qui nécessitaient de gérer les exceptions et/ou permet d'exécuter un bloc de code dans une sémantique particulière.

L'instruction "with" clarifie le code qui utilisait précédement des blocs "try ... finally" pour s'assurer de l'exécution du code de nettoyage. Dans cette section, nous allons voir cette instruction comme elle est habituellement utilisée. Dans les sections suivantes, nous examinerons les détails d'implémentation et nous verrons comment écrire des objets utilisables avec cette instruction.

L'instruction "with", décrit une nouvelle structure de contrôle de flux, qui se présente comme suit :

with expression [as var]:
with-block

L'expression est d'abord évaluée, et il devrait en résulter un objet qui supporte le protocole de gestion de contexte. Cet objet peut renvoyer une valeur qui peut facultativement être stockée dans var. (Notez que variable ne se voit pas assigner le résultat de expression !). L'objet peut alors lancer du code de mise en place avant que le bloc with ne soit exécuté et du code de nettoyage après la fin du bloc, même si le bloc lève une exception.

Pour activer l'instruction dans Python 2.5, vous devez l'importer dans votre module depuis  __future__ :

from __future__ import with_statement

L'instruction with sera toujours activée dès Python 2.6.

Certains objets standards de Python supportent déjà le protocole de gestion de contexte, et peuvent être utilisés avec l'instruction "with". L'objet File en est un exemple :

with open('/etc/passwd', 'r') as f:
for ligne in f:
print ligne
# ... le code que vous voulez ...

Après l'exécution du bloc with, l'objet fichier dans f est automatiquement fermé, même si la boucle "for" a levé une exception au millieu du bloc.

Le verrou (mutex) et les variables de condition du module threading supportent aussi l'instruction "with" :

import threading
verrou = threading.Lock()
with verrou:
# Section critique de code
...

Le verrou est acquis avant que le block ne soit exécuté, et est toujours libéré quand le bloc se finit.

La nouvelle fonction localcontext() du module "decimal" rend aisée la sauvegarde et la restauration du contexte du decimal courant, qui encapsule la précision désirée et les caractéristiques d'arrondis pour les calculs:

from decimal import Decimal, Context, localcontext

# Affiche la racine de 578 avec la précision par défaut de 28 chiffres
v = Decimal('578')
print v.sqrt()

with localcontext(Context(prec=16)):
# Tout le code dans ce bloc utilise une précision de 16 chiffres
# Le contexte original est restauré à la fin de ce bloc
print v.sqrt()

Écrire des gestionnaires de contexte

Sous le capot, l'instruction "with" est en réalité assez complexe. La plupart des développeurs utiliseront "with" avec des objets existants et ne voudront pas connaître les détails, vous pouvez donc sauter cette section si vous êtes de ceux là. Les auteurs de nouveaux objets voudront comprendre les détails de l'implémentation sous-jacente, et devraient continuer leur lecture.

Voici une explication de haut niveau du protocole de gestion de contexte :

  • L'expression est évaluée, et doit retourner un objet appelé "gestionnaire de contexte" (contexte manager). Le gestionnaire de contexte doit avoir les méthodes __enter__() et __exit()__.
  • La méthode __enter()__ du gestionnaire de contexte est appelée. La valeur retournée est assignée à VAR. Si aucune clause "as VAR" n'est présente, la valeur n'est simplement pas utilisée.
  • Le code du BLOC est exécuté.
  • Si le BLOC lève une exception, "__exit__(type, value, traceback)" est appelé avec les détails de l'exception, ces valeurs sont les mêmes que celles retournées par sys.exc_info(). La valeur de retour de la méthode contrôle si l'exception est de nouveau levée: toute valeur fausse relève l'exception, tandis que True provoquera sa suppression. Vous ne devrez que rarement supprimer une exception car dans ce cas l'auteur du bloc contenu dans le with ne verra jamais que quelque chose s'est mal passé.
  • Si le BLOC ne lève aucune exception, la méthode __exit()__ est tout de même appelée, mais type, value et traceback valent tous None.

Voyons tout ceci à l'aide d'une exemple. Je ne présenterai pas le code détaillé mais j'insisterai sur les méthodes nécessaires pour gérer une base de données qui supporte les transactions.

(Pour les personnes peu familières avec la terminologie des bases de données : un ensemble de changements d'une base de données est groupé en transaction. Les transactions peuvent être soit réalisées, tous les changements sont écrits dans la base de données, soit complètement annulées, aucun changement de la transaction ne sera appliqué et la base de donnée reste inchangée. Voyez la documentation de n'importe quelle base de données pour plus d'informations.)

Imaginez que nous ayons un objet qui représente une connexion à une base de données. Notre objectif est de permettre à l'utilisateur d'écrire ce genre de code:

db_connexion = DatabaseConnection()
with db_connexion as cursor:
cursor.execute('inster into ...')
cursor.execute('delete from ...')
# ... plus d'opérations ...

La transaction devrait être exécutée si le bloc de code est exécuté sans faille ou annulée si il y a une exception. Voici l'interface que j'utiliserais pour DatabaseConnection :

class DatabaseConnection:
# Interface de la base de donnée
def cursor (self):
"Retourne un objet curseur et commence un nouvelle transaction"
def commit (self:
"Soumet la transaction courante à la base de donnée"
def rollback (self):
"Annule la transaction courante"

La méthode __enter__() est pour le moins facile, elle n'a qu'à commencer une nouvelle transaction. Pour cette application, l'objet curseur résultant est intéressant pour la suite, nous allons donc retourner celui-ci. L'utilisateur peut alors ajouter as cursor à l'instruction with pour lier le curseur à la variable cursor.

class DatabaseConnection:
...
def __enter__ (self):
# Code pour commencer une nouvelle transaction
cursor = self.cursor()
return cursor

La méthode __exit__() est la plus compliquée car c'est là que la plupart du travail de décision est effectué. La méthode doit vérifier qu'aucune exception n'a été levée, si c'est le cas la transaction est réalisée, sinon toute la transaction doit être annulée.

Dans le code suivant, l'exécution arrive à la fin de la fonction, et retourne la valeur par défaut qui est None. None est faux, si bien que l'exception sera levée à nouveau automatiquement. Si vous le désirez, vous pouvez être plus explicite et ajouter une instruction return comme dans le commentaire.

class DatabaseConnection:
...
def __exit__(self type, value, tb):
if tb is None:
# Pas d'exception, on soumet la transaction
self.commit()
else:
# Une exception a été levée, on annule tout
self.rollback()
# return False

Le module contextlib

Le nouveau module contextlib fournit plusieurs fonctions et un décorateur utile pour écrire des objets utilisables avec l'instruction "with".

Le décorateur est contextmanager, il vous permet d'écrire une simple fonction générateur pour définir une nouvelle classe. Le générateur ne peut suspendre avec yield qu'une seule valeur. Le code avant yield sera exécuté comme méthode __enter__(), et la valeur suspendue avec yield sera la valeur de retour qui pourra être liée à une variable à l'aide de la clause as dans l'instruction with. Le code après yield sera exécuté comme méthode __exit__(). Toute exception levée dans le bloc sera levé par yield(voir PEP 342: Nouvelles fonctionnalités des générateurs).

Notre exemple de base de données de la section précédente peut être réécrit comme suit à l'aide du décorateur:

from contextlib import contextmanager

@contextmanager
def db_transaction (connection):
cursor = connecton.cursor()
try:
yield cursor
except:
connection.rollback()
raise
else:
connection.commit()
db = DatabaseConnection
with db_transaction(db) as cursor:
...

Le module contextlib introduit aussi une fonction nested(mgr1, mgr2, ...) qui combine plusieurs gestionnaires de contexte pour que vous n'ayez pas à écrire de nombreuses instructions with imbriquées. Dans l'exemple suivant, une simple instruction with commence une connexion à une base de données et obtient un verrou pour un thread en même temps.

import threading
from contextlib import nested

verrou = threading.Lock()
with nested (db_transaction(db), lock) as (cursor, locked):
...

Pour finir, la nouvelle fonction closing(object) retourne object qui peut être lié à une variable, et appelle object.close() a la fin du bloc with :

import urllib, sys
from contextlib import closing

with closing(urllib.urlopen('http://www.LinuxCertif.com')) as f:
for ligne in f:
sys.stdout.write(ligne)
Actions sur le document