使用装饰器进行函数类型检查

动态类型的特性使得Python函数在被调用时,其参数类型不易被知晓。或者,为了动态支持多类型,实际参数的类型由调用者提供。如下:

def add(x, y):
    return x + y

print(add(2, 3)) # 5
print(add('Hello', ' World')) # Hello World

上面的例子可以看出,函数参数并没有指定类型,使得该函数支持多种类型,这也正是Python语言的特殊之处。

但有时候,我们想限制函数的参数类型。这时很多人会想到类型提示(Type Hinting),即类型注解。如下:

def add(x:str, y:str) -> str:
    return x + y

然而,类型提示仅仅作为编程规约。在实际调用中无法强制类型约束,也不会有任何报错,如下:

print(add(2, 3)) # 5

若要强制类型检查,只能在编程中进行类型检查,然后进行异常提示。代码如下:

from inspect import signature
from functools import wraps


def typeassert(*ty_args, **ty_kwargs):
    def decorate(func):
        # If in optimized mode, disable type checking
        if not __debug__:
            return func

        # Map function argument names to supplied types
        sig = signature(func)
        bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments

        @wraps(func)
        def wrapper(*args, **kwargs):
            bound_values = sig.bind(*args, **kwargs).arguments
            # Enforce type assertions across supplied arguments
            for name, value in bound_values.items():
                if name in bound_types:
                    if not isinstance(value, bound_types[name]):
                        raise TypeError('Argument {} must be {}'.format(name, bound_types[name]))
            return func(*args, **kwargs)
        return wrapper
    return decorate


@typeassert(int, int)
def add(x, y):
    return x + y


@typeassert(int, z=int)
def spam(x, y, z=42):
    print(x, y, z)


>>> spam(1, 2, 3)
12 3
>>> spam(1, 'hello', 3)
1 hello 3
>>> spam(1, 'hello', 'world') T
raceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "contract.py", line 33, in wrapper 
TypeError: Argument z must be <class 'int'> 
>>>

编写此装饰器的一个棘手的部分是,它涉及检查并使用要包装的函数的参数签名。 此处选择的工具应该是inspect.signature()函数。 简而言之,它允许从可调用对象中提取签名信息。
如下:

>>> from inspect import signature 
>>> def spam(x, y, z=42):
...     pass
...
>>> sig = signature(spam)
>>> print(sig)
(x, y, z=42)
>>> sig.parameters
mappingproxy({'x': <Parameter "x">,
              'y': <Parameter "y">,
              'z': <Parameter "z=42">})
>>> sig.parameters['z'].name
'z'
>>> sig.parameters['z'].default 
42
>>> sig.parameters['z'].kind 
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>
>>>

在装饰器的第一部分,使用signature对象的bind_partial()方法对提供的类型与参数名称进行部分绑定。

>>> bound_types = sig.bind_partial(int,z=int) 
>>> bound_types
<BoundArguments (x=<class 'int'>, z=<class 'int'>)>
>>> bound_types.arguments
OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])
>>>

在此部分绑定中,会注意到丢失的参数将被忽略(即,参数y没有绑定)。 但是,绑定的最重要部分是创建有序字典bound_types.arguments。 该字典以与函数签名相同的顺序将参数名称映射到提供的值。 对于我们的装饰器,此映射包含我们要强制执行的类型断言。

在装饰器执行的实际包装函数中,使用sig.bind()方法。 bind()类似于bind_partial(),不同之处在于它不允许缺少参数。 因此,发生了以下情况:

>>> bound_values = sig.bind(1, 2, 3)
>>> bound_values.arguments 
OrderedDict([('x', 1), ('y', 2), ('z', 3)]) 
>>>
posted @ 2020-02-03 21:42  Jeffrey_Yang  阅读(285)  评论(0编辑  收藏  举报