web-dev-qa-db-fra.com

Comment utiliser les décorateurs Python pour vérifier les arguments des fonctions?

Je voudrais définir quelques décorateurs génériques pour vérifier les arguments avant d'appeler des fonctions.

Quelque chose comme:

@checkArguments(types = ['int', 'float'])
def myFunction(thisVarIsAnInt, thisVarIsAFloat)
    ''' Here my code '''
    pass

Notes de côté:

  1. La vérification de type est juste ici pour montrer un exemple
  2. J'utilise Python 2.7 mais Python 3.0 serait également intéressant
27
AsTeR

De la Décorateurs pour fonctions et méthodes :

def accepts(*types):
    def check_accepts(f):
        assert len(types) == f.func_code.co_argcount
        def new_f(*args, **kwds):
            for (a, t) in Zip(args, types):
                assert isinstance(a, t), \
                       "arg %r does not match %s" % (a,t)
            return f(*args, **kwds)
        new_f.func_name = f.func_name
        return new_f
    return check_accepts

Usage:

@accepts(int, (int,float))
def func(arg1, arg2):
    return arg1 * arg2

func(3, 2) # -> 6
func('3', 2) # -> AssertionError: arg '3' does not match <type 'int'>
28
jfs

Sur Python 3.3, vous pouvez utiliser les annotations de fonctions et inspecter:

import inspect

def validate(f):
    def wrapper(*args):
        fname = f.__name__
        fsig = inspect.signature(f)
        vars = ', '.join('{}={}'.format(*pair) for pair in Zip(fsig.parameters, args))
        params={k:v for k,v in Zip(fsig.parameters, args)}
        print('wrapped call to {}({})'.format(fname, params))
        for k, v in fsig.parameters.items():
            p=params[k]
            msg='call to {}({}): {} failed {})'.format(fname, vars, k, v.annotation.__name__)
            assert v.annotation(params[k]), msg
        ret = f(*args)
        print('  returning {} with annotation: "{}"'.format(ret, fsig.return_annotation))
        return ret
    return wrapper

@validate
def xXy(x: lambda _x: 10<_x<100, y: lambda _y: isinstance(_y,float)) -> ('x times y','in X and Y units'):
    return x*y

xy = xXy(10,3)
print(xy)

S'il y a une erreur de validation, affiche:

AssertionError: call to xXy(x=12, y=3): y failed <lambda>)

S'il n'y a pas d'erreur de validation, affiche:

wrapped call to xXy({'y': 3.0, 'x': 12})
  returning 36.0 with annotation: "('x times y', 'in X and Y units')"

Vous pouvez utiliser une fonction plutôt qu'un lambda pour obtenir un nom lors de l'échec de l'assertion. 

14
dawg

Comme vous le savez certainement, ce n'est pas Pythonic de rejeter un argument uniquement en fonction de son type.
L’approche Pythonic est plutôt "essayez d’y faire face en premier"
C'est pourquoi je préférerais faire un décorateur pour convertir les arguments

def enforce(*types):
    def decorator(f):
        def new_f(*args, **kwds):
            #we need to convert args into something mutable   
            newargs = []        
            for (a, t) in Zip(args, types):
               newargs.append( t(a)) #feel free to have more elaborated convertion
            return f(*newargs, **kwds)
        return new_f
    return decorator

De cette façon, votre fonction est alimentée avec le type que vous attendez Mais si le paramètre peut trembler comme un float, il est accepté

@enforce(int, float)
def func(arg1, arg2):
    return arg1 * arg2

print (func(3, 2)) # -> 6.0
print (func('3', 2)) # -> 6.0
print (func('three', 2)) # -> ValueError: invalid literal for int() with base 10: 'three'

J'utilise cette astuce (avec la méthode de conversion appropriée) pour traiter vectors .
De nombreuses méthodes que j'écris attendent de la classe MyVector car elle comporte de nombreuses fonctionnalités; mais parfois vous voulez juste écrire 

transpose ((2,4))
9
Madlozoz

Pour appliquer des arguments de chaîne à un analyseur syntaxique pouvant générer des erreurs cryptiques avec une entrée non chaîne, j'ai écrit ce qui suit pour éviter les appels d'allocation et de fonction:

from functools import wraps

def argtype(**decls):
    """Decorator to check argument types.

    Usage:

    @argtype(name=str, text=str)
    def parse_rule(name, text): ...
    """

    def decorator(func):
        code = func.func_code
        fname = func.func_name
        names = code.co_varnames[:code.co_argcount]

        @wraps(func)
        def decorated(*args,**kwargs):
            for argname, argtype in decls.iteritems():
                try:
                    argval = args[names.index(argname)]
                except ValueError:
                    argval = kwargs.get(argname)
                if argval is None:
                    raise TypeError("%s(...): arg '%s' is null"
                                    % (fname, argname))
                if not isinstance(argval, argtype):
                    raise TypeError("%s(...): arg '%s': type is %s, must be %s"
                                    % (fname, argname, type(argval), argtype))
            return func(*args,**kwargs)
        return decorated

    return decorator
2
jbouwman

Toutes ces publications semblent obsolètes - pint fournit désormais cette fonctionnalité intégrée. Voir ici . Copié ici pour la postérité:

Vérification de la dimensionnalité Lorsque vous souhaitez utiliser les quantités de pinte comme entrées pour vos fonctions, pint fournit un wrapper pour s’assurer que les unités sont de type correct - ou plus précisément, ils correspondent à l'attendu dimensionnalité de la quantité physique.

Semblable à wraps (), vous pouvez passer Aucun pour ignorer la vérification de certains paramètres, mais le type de paramètre de retour n'est pas vérifié.

>>> mypp = ureg.check('[length]')(pendulum_period) 

Dans le format décorateur:

>>> @ureg.check('[length]')
... def pendulum_period(length):
...     return 2*math.pi*math.sqrt(length/G)
1
Ethan Keller
def decorator(function):
    def validation(*args):
        if type(args[0]) == int and \
        type(args[1]) == float:
            return function(*args)
        else:
            print('Not valid !')
    return validation
0
mujad

J'ai une version légèrement améliorée de @jbouwmans sollution, en utilisant le module de décorateur en python, ce qui rend le décorateur entièrement transparent et conserve non seulement la signature, mais aussi la documentation, et peut-être la manière la plus élégante d'utiliser des décorateurs.

from decorator import decorator

def check_args(**decls):
    """Decorator to check argument types.

    Usage:

    @check_args(name=str, text=str)
    def parse_rule(name, text): ...
    """
    @decorator
    def wrapper(func, *args, **kwargs):
        code = func.func_code
        fname = func.func_name
        names = code.co_varnames[:code.co_argcount]
        for argname, argtype in decls.iteritems():
            try:
                argval = args[names.index(argname)]
            except IndexError:
                argval = kwargs.get(argname)
            if argval is None:
                raise TypeError("%s(...): arg '%s' is null"
                            % (fname, argname))
            if not isinstance(argval, argtype):
                raise TypeError("%s(...): arg '%s': type is %s, must be %s"
                            % (fname, argname, type(argval), argtype))
    return func(*args, **kwargs)
return wrapper
0
MaxFragg

Je pense que la réponse de Python 3.5 à cette question est beartype . Comme expliqué dans ce post , il comporte des fonctionnalités pratiques. Votre code ressemblerait alors à ceci

from beartype import beartype
@beartype
def sprint(s: str) -> None:
   print(s)

et aboutit à

>>> sprint("s")
s
>>> sprint(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 13, in func_beartyped
TypeError: sprint() parameter s=3 not of <class 'str'>
0
Iwan LD