使用装饰器进行函数类型检查
动态类型的特性使得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)])
>>>