Jak używać dekoratorów Pythona do sprawdzania argumentów funkcji?

Chciałbym zdefiniować kilka ogólnych dekoratorów, aby sprawdzać argumenty przed wywołaniem niektórych funkcji.

Coś w stylu:

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

Uwagi boczne:

  1. sprawdzanie typu jest tylko tutaj, aby pokazać przykład
  2. używam Pythona 2.7, ale Python 3.0 też byłby interesujący
Author: AsTeR, 2013-03-08

7 answers

Z dekoratorów funkcji i metod :

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

Użycie:

@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'>
 27
Author: jfs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-08 17:49:24

W Pythonie 3.3 możesz użyć adnotacji funkcji i inspect:

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)

Jeśli wystąpi błąd walidacji, wyświetla:

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

Jeśli nie ma błędu walidacji, drukuje:

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

Możesz użyć funkcji zamiast lambda, aby uzyskać nazwę w niepowodzeniu twierdzenia.

 14
Author: dawg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-08 17:51:08

Jak na pewno wiesz, nie jest pythoniczne odrzucanie argumentu tylko na podstawie jego typu.
Podejście pythoniczne jest raczej "spróbuj najpierw poradzić sobie z tym"
Dlatego wolałbym zrobić dekoratora, aby przekonwertować argumenty

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

W ten sposób twoja funkcja jest zasilana typem, którego oczekujesz Ale jeśli parametr może kwakać się jak float, jest akceptowany

@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'

Używam tej sztuczki (z właściwą metodą konwersji) do radzenia sobie z wektorami.
Wiele metod, które piszę oczekuję Klasa MyVector, ponieważ ma mnóstwo funkcjonalności, ale czasami po prostu chcesz napisać

transpose ((2,4))
 7
Author: Madlozoz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-10 09:37:58

Aby wyegzekwować argumenty łańcuchowe do parsera, który wyrzuciłby tajemnicze błędy, gdy dostarczono dane wejściowe nie-łańcuchowe, napisałem następujący tekst, który próbuje uniknąć alokacji i wywołań funkcji:

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
Author: jbouwman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-22 18:19:46

Wszystkie te posty wydają się nieaktualne - kufel udostępnia teraz tę funkcjonalność wbudowaną. Zobacz TUTAJ . Skopiowane tutaj dla potomności:

Sprawdzanie wymiarowości, gdy chcemy, aby ilości piwa były używane jako wejścia do swoich funkcji, kufel zapewnia owijarkę, która zapewnia, że jednostki są właściwego typu - a dokładniej odpowiadają oczekiwanym wymiarowość ilości fizycznej.

Podobnie jak wraps (), możesz przekazać None, aby pominąć sprawdzanie niektórych parametry, ale typ parametru return nie jest sprawdzany.

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

W formacie dekoratora:

>>> @ureg.check('[length]')
... def pendulum_period(length):
...     return 2*math.pi*math.sqrt(length/G)
 1
Author: Ethan Keller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-21 18:46:15

Mam nieco ulepszoną wersję @ jbouwmans sollution, używającą modułu dekoratora Pythona, który sprawia, że dekorator jest w pełni przezroczysty i utrzymuje nie tylko podpis, ale także docstringi na miejscu i może być najbardziej eleganckim sposobem korzystania z dekoratorów

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
Author: MaxFragg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-01 19:50:13

Myślę, że odpowiedź Pythona 3.5 na to pytanie to beartype . Jak wyjaśniono w tym post jest wyposażony w przydatne funkcje. Twój kod będzie wyglądał tak:

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

I wyniki w

>>> 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
Author: Iwan LD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:25:19