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

Posted

技术标签:

【中文标题】如何使用 Python 装饰器检查函数参数?【英文标题】:How to use Python decorators to check function arguments? 【发布时间】:2013-02-24 08:16:26 【问题描述】:

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

类似:

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

旁注:

    类型检查只是在这里展示一个示例 我使用的是 Python 2.7,但 Python 3.0 应该也很有趣

EDIT 2021:有趣的是,从长远来看,type hinting 和 mypy 的类型检查并没有反python。

【问题讨论】:

请注意,这通常是一个非常糟糕的主意——它违背了 Python 的本质。在几乎所有情况下,类型检查都是一件坏事。还值得注意的是,如果您在 3.x 中,使用参数注释来执行此操作可能更有意义。 @Lattyware:强制函数参数和返回类型是the original pep for decorators中的示例之一 @Lattyware:这不是pythonic,但如果你真的想这样做,装饰器和参数注释是最好的方法。 你们是在拖钓吗?还是我在这个哲学问题上变得很敏感? ;) 我不是在拖钓,我只是在说明大多数情况下,如果您进行类型检查,那么您做错了,最好换一种方式。经常看到人们进行 SO 类型检查并生成不灵活的函数,这些函数由于类型检查而无法正常工作或效率高。 【参考方案1】:

来自Decorators for Functions and Methods:

Python 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

在 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

用法:

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

arg2 可以是intfloat

【讨论】:

我在某些方法上使用它,但似乎 f 始终具有最后定义的函数的值。你知道这可能来自哪里吗? @AsTeR:创建一个重现您的问题的minimal complete code 示例和post it as a new question。 我推荐使用这个方案,如果有很多输入参数,它具有很好的可读性。 code.activestate.com/recipes/… @IAbstract 在 Python 3 中,func_code(据我所知)已被替换为魔术属性 __code__ @user4447514 func_name 也被替换为__name__【参考方案2】:

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

如果有验证错误,打印:

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

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

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

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

【讨论】:

看起来很有趣,但乍一看真的很难理解。等我不累的时候再看看。 这是一个难以置信的混淆实现。从技术上讲,它有效。但它会让眼睛流血。要获得更易读(尽管功能稍弱)的替代方案,请参阅sweeneyrod 的简洁@checkargs decorator 在similar question 下。 @CecilCurry:你能详细说明为什么你认为它如此糟糕吗?我认为通过 lambda 进行检查非常明智。【参考方案3】:

您当然知道,仅根据参数类型拒绝参数并不是 Python 语言。 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

这样,你的函数就会得到你期望的类型 但如果参数可以像浮点数一样嘎嘎作响,则被接受

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

我用这个技巧(用正确的转换方法)来处理vectors。 我编写的许多方法都期望 MyVector 类,因为它有很多功能;但有时你只想写

transpose ((2,4))

【讨论】:

“您当然知道,仅根据参数类型拒绝参数并不是 Pythonic。”。你有这方面的参考吗? 我相信他指的是“鸭子打字”,如果它像鸭子一样嘎嘎叫,像鸭子一样走路,那么它就是鸭子......但是我会与原始类型争论,例如浮点数和小数,例如。 Decimal(1.3)Decimal('1.3') 不一样 如果我打电话给func(3, arg2=2),这个解决方案不会中断吗?然后,3*args 中,2**kwargs 中,因为后者是作为关键字参数提供的。【参考方案4】:

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

【讨论】:

【参考方案5】:

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

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

【讨论】:

我最终使用了这个:相对简单,仅使用标准库,并且可以使用可变数量的 *args 和 **kwargs。唯一需要注意的是,func_code 在 Python 3 中被重命名为 __code__,我不知道是否有跨版本的方式来做到这一点。【参考方案6】:

我有一个稍微改进的@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

【讨论】:

【参考方案7】:

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

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

结果

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

【讨论】:

【参考方案8】:

所有这些帖子似乎都已过时 - pint 现在内置了此功能。请参阅 here。为后代复制这里:

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

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

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

在装饰器格式中:

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

【讨论】:

【参考方案9】:

您可以尝试使用pydantic validation_decorator。来自文档 pydantic

使用 python 类型注释的数据验证和设置管理。 pydantic 在运行时强制执行类型提示,并提供用户友好的 数据无效时出错。 在benchmarks 中,pydantic 比所有其他经过测试的库都快。

from pydantic import validate_arguments, ValidationError


@validate_arguments
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
    b = s.encode()
    return separator.join(b for _ in range(count))


a = repeat('hello', 3)
print(a)
#> b'hellohellohello'

b = repeat('x', '4', separator=' ')
print(b)
#> b'x x x x'

try:
    c = repeat('hello', 'wrong')
except ValidationError as exc:
    print(exc)
    """
    1 validation error for Repeat
    count
      value is not a valid integer (type=type_error.integer)
    """

【讨论】:

【参考方案10】:

对我来说,上面共享的代码看起来很复杂。我为类型检查定义“通用装饰器”所做的工作:

我使用了 *args、**kwargs 功能,使用函数/方法时的额外工作很少,但易于管理。

适当的测试示例定义

argument_types = 
'name':str,
'count':int,
'value':float

装修定义

//from functools import wraps

def azure_type(func):
    @wraps(func)
    def type_decorator(*args, **kwargs):
        for key, value in kwargs.items():
            if key in argument_types:
                if type(value) != argument_types[key]:
                    #enter code here
                    return 'Error Message or what ever you like to do'  
        return func(*args, **kwargs)
    return type_decorator 

代码中的简单示例

// all other definitions

@azure_type
def stt(name:str, value:float)->(int):
    #some calculation and creation of int output 
    count_output = #something int
    return count_output

// call the function:

stt(name='ati', value=32.90) #can test from that

【讨论】:

以上是关于如何使用 Python 装饰器检查函数参数?的主要内容,如果未能解决你的问题,请参考以下文章

python_如何定义带参数的装饰器?

内部装饰器函数如何查看装饰函数参数? [复制]

python进阶之装饰器之2.定义一个可接受参数的装饰器如何定义一个属性可由用户修改的装饰器定义一个能接受可选参数的装饰器

在 Python 中,如何获取带参数传递给装饰器的函数名称?

python_如何修改装饰器中参数?

Python装饰器