Outils pour utilisateurs

Outils du site


decorateurs_verifargs

Vérification par décorateur des arguments passés à l'appel de fonctions

Problématique

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):

  • on puisse récupérer l'erreur pour qu'elle ne “plante” pas la calculatrice,
  • on puisse renvoyer à l'utilisateur un message explicite sur l'erreur qu'il a commise afin qu'il puisse la corriger.

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

Décorateur proposé

On définit un décorateur qui s'appelle ici verifargs(). Ce décorateur peut recevoir des arguments qui sont de 2 types:

  • pour les arguments par position: des chaines de caractères représentant des expressions booléennes qui doivent être vraies (ex: “n>0”)
  • pour les arguments par défaut: les types à vérifier (ex: n=int)

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!

Codage

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

Commentaires sur le code

Dans l'exécution, ça se passe en 2 phases:

  • initialisation: exécution de __init__ et __call__ (mais pas appelfonc) pendant la lecture/compilation du code
  • exécution de appelfonc à chaque appel de la fonction décorée: exécution des vérifications demandées avant l'exécution de la fonction.


Amusez-vous bien!

decorateurs_verifargs.txt · Dernière modification: 2011/04/04 21:19 de tyrtamos

Outils de la page