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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15299878/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-18 19:42:57  来源:igfitidea点击:

How to use Python decorators to check function arguments?

pythonpython-decorators

提问by AsTeR

I would like to define some generic decorators to check arguments before calling some functions.

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

Something like:

就像是:

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

Side notes:

旁注:

  1. Type checking is just here to show an example
  2. I'm using Python 2.7 but Python 3.0 whould be interesting too
  1. 类型检查只是为了展示一个例子
  2. 我正在使用 Python 2.7 但 Python 3.0 也应该很有趣

采纳答案by jfs

From the Decorators for Functions and Methods:

来自函数和方法装饰器

Python 2

蟒蛇 2

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

Python 3

蟒蛇 3

In Python 3 func_codehas changed to __code__and func_namehas changed to __name__.

在 Python 3 中func_code已更改为__code__并且func_name已更改为__name__.

def accepts(*types):
    def check_accepts(f):
        assert len(types) == f.__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.__name__ = f.__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'>

arg2can be either intor float

arg2可以是intfloat

回答by dawg

On Python 3.3, you can use function annotations and inspect:

在 Python 3.3 上,您可以使用函数注释并检查:

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)

If there is a validation error, prints:

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

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

If there is not a validation error, prints:

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

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

You can use a function rather than a lambda to get a name in the assertion failure.

您可以使用函数而不是 lambda 来获取断言失败中的名称。

回答by jbouwman

To enforce string arguments to a parser that would throw cryptic errors when provided with non-string input, I wrote the following, which tries to avoid allocation and function calls:

为了强制解析器的字符串参数在提供非字符串输入时会抛出神秘错误,我写了以下内容,试图避免分配和函数调用:

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

回答by MaxFragg

I have a slightly improved version of @jbouwmans sollution, using python decorator module, which makes the decorator fully transparent and keeps not only signature but also docstrings in place and might be the most elegant way of using decorators

我有一个稍微改进的@jbouwmans 解决方案,使用 python 装饰器模块,这使得装饰器完全透明,不仅保持签名而且保持文档字符串到位,这可能是使用装饰器的最优雅的方式

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

回答by Madlozoz

As you certainly know, it's not pythonic to reject an argument only based on its type.
Pythonic approach is rather "try to deal with it first"
That's why I would rather do a decorator to convert the arguments

您当然知道,仅根据参数类型拒绝参数并不是 Pythonic。
Pythonic 的方法是“尝试先处理它”
这就是为什么我宁愿做一个装饰器来转换参数

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

This way, your function is fed with the type you expect But if the parameter can quack like a float, it is accepted

这样,您的函数就会使用您期望的类型但是如果参数可以像浮点数一样嘎嘎作响,它就会被接受

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

I use this trick (with proper conversion method) to deal with vectors.
Many methods I write expect MyVector class as it has plenty of functionalities; but sometime you just want to write

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

transpose ((2,4))

回答by Iwan LD

I think the Python 3.5 answer to this question is beartype. As explained in this postit comes with handy features. Your code would then look like this

我认为这个问题的 Python 3.5 答案是beartype。正如这篇文章中所解释的,它具有方便的功能。您的代码将如下所示

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

and results in

并导致

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

回答by Ethan Keller

All of these posts seem out of date - pint now provides this functionality built in. See here. Copied here for posterity:

所有这些帖子似乎都过时了 - 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.

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

In the decorator format:

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

检查维度 当您希望将品脱数量用作函数的输入时,品脱提供了一个包装器以确保单位的类型正确 - 或者更准确地说,它们与物理量的预期维度相匹配。

与 wraps() 类似,您可以通过 None 跳过某些参数的检查,但不检查返回参数类型。

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

在装饰器格式中:

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

回答by Facundo

The package typeguardprovides a decorator for this, it reads the type information from type annotations, it requires Python >=3.5.2 though. I think the resulting code is quite nice.

该包typeguard为此提供了一个装饰器,它从类型注释中读取类型信息,但它需要 Python >=3.5.2。我认为生成的代码非常好。

@typeguard.typechecked
def my_function(this_var_is_an_int: int, this_var_is_a_float: float)
    ''' Here my code '''
    pass