Si on prend l'exemple d'une calculatrice, toutes les fonctions de calcul peuvent être appelées avec des données saisies par l'utilisateur. Il est alors important qu'en cas d'erreur à cause de données anormales (ex: racine carrée d'un nombre négatif):
C'est le cas pour la vérification de conditions sur les arguments (ex: n>0) et sur leurs types (ex: type(n)=int). Le problème est que ces vérifications doivent être codées dans chacune des fonctions, ce qui prend du temps et de la place. D'autant que l'on n'a pas toujours accès au code des fonctions (ex: compilées en C), ce qui oblige à créer des fonctions enveloppes.
L'objectif de cette page est de permettre ces vérifications des arguments passés aux fonctions et méthodes en utilisant les décorateurs.
Les décorateurs sont des fonctions ou des classes qui permettent de modifier des fonctions ou des méthodes par des opérations faites avant et/ou après leur appel.
Voir la documentation Python, et, par exemple: http://wiki.python.org/moin/PythonDecoratorLibrary
On définit un décorateur qui s'appelle ici verifargs(). Ce décorateur peut recevoir des arguments qui sont de 2 types:
Exemple d'utilisation:
@verifargs("a>=b", "c>=0", b=int, c=(int,long,float), d=(NoneType,str,unicode)) def fonctiontest(a, b, c, d="toto", e=123): return a*b*c*e, d
On voit bien ici les 2 types de vérification à faire: les conditions (“a>=b”, “c>=0”) et les vérifications de type (b=int, c=(int,long,float), d=(NoneType,str,unicode))
On voit aussi qu'on n'est pas obligé de tout vérifier: ici, le type des variables a et c n'est pas vérifié
Quelques exemples d'appel de cette fonction:
r = fonctiontest(6, 5, 3.123, d=None) # appel réussi: toutes les conditions et tous les types sont ok # à noter que le type de la valeur None est vérifié grâce à NoneType du module standard types r = fonctiontest(2, "truc", 3, d="toto") # échec: mauvais type pour b # à noter que la vérification sur les types est faite avant l'évaluation des conditions r = fonctiontest(2, 5, 3, d=12.56) # échec: condition a>=b non remplie # à noter que les types des arguments par défaut non passés (ici e=123) ne sont pas testés r = fonctiontest(6, 5, -3.123, d=None) # échec: condition c>=0 non remplie
Ça marche aussi de la même façon pour les méthodes des classes:
class Classetest(object): @verifargs("a>=b", "c>=0", b=int, c=(int,long,float), d=(NoneType,str,unicode)) def methodetest(self, a, b, c, d="toto", e=123): return a*b*c*e, d x = Classetest() r = x.methodetest(2, 5, 3.123, d=None) # échec: condition a>=b non remplie
On peut, bien entendu, récupérer proprement l'exception générée:
try: r = x.methodetest(5, 5, 3.123, d=None) except (ValueError, TypeError) as e: print e
Bien sûr, si on mentionne dans le décorateur des variables qui n'existent pas dans les arguments de la fonction, une exception ValueError est générée:
@verifargs("a>=b", "c>=0", b=int, c2=(int,long,float), d=(NoneType,str,unicode)) def fonctiontest(a, b, c, d="toto"): print a*b*c, d # génère à l'initialisation une exception ValueError puisque c2 n'existe pas dans les variables d'appel de la fonction
A noter que le decorateur étant conçu pour avoir des arguments, il doit être passé avec des parenthèses, même s'il n'y a pas d'argument. Ce qui, bien sûr, n'a pas d'intérêt ici:
@verifargs() def fonctiontest(a, b, c, d="toto"): print a*b*c, d # ce décorateur ne fait rien mais reste syntaxiquement correct!
La solution choisie ici est d'utiliser une classe pour le décorateur. Cela semble plus compliqué que les versions habituelles de fonctions multi-imbriquées, mais on s'y retrouve si on applique un modèle général qui permet de tout faire.
#! /usr/bin/python # -*- coding: utf-8 -*- # Python v2.7 from types import NoneType # pour tester le type de la valeur 'None' class verifargs(object): """Décorateur pour vérification des arguments dans l'appel de fonctions""" def __init__(self, *decargs, **deckwargs): """initialisation du décorateur """ # arg. par position: liste des conditions à remplir self.decargs = decargs # arg par défaut: dico des "variable:type(s)" pour les types à tester self.deckwargs = deckwargs def __call__(self, fonc): def appelfonc(*args, **kwargs): """méthode appelée à chaque appel de la fonction décorée""" # dico des "variable:valeur" passées à la fonction dicvars = dict(zip(vars[:len(args)],args)) dicvars.update(kwargs) for v in dvars_def: if v not in kwargs: dicvars[v] = dvars_def[v] # vérif. des types pour les var. par défaut passées au décorateur for decvar in self.deckwargs: # vérif. que ces variables existent dans les var. d'appel de la fonction if decvar not in vars: raise ValueError ("Erreur decorateur verifargs sur %s: la variable %s n'existe pas" % (nomfonc, decvar)) # vérif. que les variables passées au décorateur ont le bon type if decvar in dicvars: val = dicvars[decvar] typ = self.deckwargs[decvar] if not isinstance(val, typ): raise TypeError ("Erreur appel %s: mauvais type pour %s" % (nomfonc, decvar)) # vérif. des conditions à remplir sur les arguments par position du décorateur for decarg in self.decargs: if not eval(decarg, dicvars): raise ValueError ("Erreur appel %s: échec condition %s" % (nomfonc, decarg)) # appel de fonc avec tous ses arguments, et retour du résultat return fonc(*args, **kwargs) # nom de la fonction décorée (pour les messages suite à exception) nomfonc = fonc.func_name # liste de toutes les variables d'appel de la fonction décorée vars = list(fonc.func_code.co_varnames[:fonc.func_code.co_argcount]) # liste des valeurs par defaut args_def = fonc.func_defaults if argsfn_def == None: argsfn_def = () # liste des variables par défaut vars_def = vars[len(vars)-len(args_def):] # création du dictionnaire des variables:valeurs par défaut dvars_def = dict(zip(vars_def, args_def)) return appelfonc
Dans l'exécution, ça se passe en 2 phases:
Amusez-vous bien!