如何使用Python装饰器检查函数参数?

How to use Python decorators to check function arguments?

我想在调用一些函数之前定义一些通用的修饰符来检查参数。

类似:

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

侧记:

  • 类型检查只是为了显示一个示例
  • 我使用的是python 2.7,但是python 3.0也很有趣

  • 从函数和方法的decorator:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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

    用途:

    1
    2
    3
    4
    5
    6
    @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'>


    在Python3.3上,可以使用函数注释并检查:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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)

    如果存在验证错误,则打印:

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

    如果没有验证错误,则打印:

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

    在断言失败时,可以使用函数而不是lambda来获取名称。


    正如你所知道的,仅仅根据一个参数的类型来拒绝它并不是Python式的。Python疗法相当于"先处理它"这就是为什么我宁愿做一个修饰器来转换参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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

    这样,您的函数就得到了您期望的类型但是,如果参数可以像浮点数一样抖动,则可以接受

    1
    2
    3
    4
    5
    6
    7
    @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'

    我使用这个技巧(使用适当的转换方法)来处理向量。我编写的许多方法都期望MyVector类具有许多功能;但有时您只想编写

    1
    transpose ((2,4))


    为了对解析器强制使用字符串参数,当提供非字符串输入时,这些参数会引发神秘的错误,我编写了以下内容,试图避免分配和函数调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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


    所有这些帖子似乎都过时了-Pint现在提供了内置的功能。请看这里。为子孙后代复制到这里:

    Checking dimensionality When you want pint quantities to be used as
    inputs to your functions, pint provides a wrapper to ensure units are
    of correct type - or more precisely, they match the expected
    dimensionality of the physical quantity.

    Similar to wraps(), you can pass None to skip checking of some
    parameters, but the return parameter type is not checked.

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

    In the decorator format:

    1
    2
    3
    >>> @ureg.check('[length]')
    ... def pendulum_period(length):
    ...     return 2*math.pi*math.sqrt(length/G)

    1
    2
    3
    4
    5
    6
    7
    8
    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


    我认为python 3.5对这个问题的答案是beartype。正如本文中所解释的,它有一些方便的特性。然后您的代码将如下所示

    1
    2
    3
    4
    from beartype import beartype
    @beartype
    def sprint(s: str) -> None:
       print(s)

    结果

    1
    2
    3
    4
    5
    6
    7
    >>> 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'>

    我有一个稍微改进的@jbouwmans解决方案版本,它使用python decorator模块,使decorator完全透明,不仅保留签名,而且保留文档字符串,这可能是使用decorator的最优雅的方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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