前言
由于 python 不支持函数入参类型的限定,所以,对于 python 来说,入参合法性检测显得尤为重要。
比如,我们要实现一个整数的加法运算函数,要进行入参检测,一般会这么写,非常的不 pythonic。
def add(x, y):
if not isinstance(x, int):
raise Exception
if not isinstance(y, int):
raise Exception
return x + y
def testcases():
print(add(3, 4))
print(add(4.5, 3.2))
if __name__ == '__main__':
testcases()
要想改一下代码,其实也没优雅到哪去,半斤八两。
def add(x, y):
if not all(isinstance(item, int) for item in [x, y]):
raise Exception
return x + y
def testcases():
print(add(3, 4))
print(add(4.5, 3.2))
if __name__ == '__main__':
testcases()
铺垫
今天,为大家介绍一种 pythonic 的入参合法性校验方法。介绍这个方法之前,需要介绍一下 PEP 3107 之 Function Annotations。这个是 python3 才支持的一个特性,可以为函数的参数进行注解,如下所示。
def compile(source: "something compilable",
filename: "where the compilable thing comes from",
mode: "is this a single statement or a suite?"):
...
当然,也可以对函数的返回值进行注解,如下所示。
def haul(item: Haulable, *vargs: PackAnimal) -> "Distance":
...
这个很有用,别人看到你的源码的时候,通过注解,就可以你这个参数应该传什么进去,还有就是,当别人用 help 来显示你写的函数的 docstring 时,这些注释也会显示。
>>> def add(x: 'integer', y: 'integer') -> 'the sum':
... return x + y
...
>>> help(add)
Help on function add in module __main__:
add(x:'integer', y:'integer') -> 'the sum'
实际上,这些注释是藏在 __annotations__ 字段里的,通过如下指令可以显示。
>>> add.__annotations__
{'y': 'integer', 'x': 'integer', 'return': 'the sum'}
其实,不光是字符串,字典 __annotations__ 里的值可以是任何 object,可以是 list,可以是 tuple,甚至,可以是函数。
正文
到此为止,介绍一下更加 pythonic 的入参合法性校验方法,代码如下所示。
import functools
import inspect
def auto_type_checker(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
# fetch the argument name list.
parameters = inspect.signature(function).parameters
argument_list = list(parameters.keys())
# fetch the argument checker list.
checker_list = [parameters[argument].annotation for argument in argument_list]
# fetch the value list.
value_list = [inspect.getcallargs(function, *args, **kwargs)[argument] for argument in inspect.getfullargspec(function).args]
# initialize the result dictionary, where key is argument, value is the checker result.
result_dictionary = dict()
for argument, value, checker in zip(argument_list, value_list, checker_list):
result_dictionary[argument] = check(argument, value, checker, function)
# fetch the invalid argument list.
invalid_argument_list = [key for key in argument_list if not result_dictionary[key]]
# if there are invalid arguments, raise the error.
if len(invalid_argument_list) > 0:
raise Exception(invalid_argument_list)
# check the result.
result = function(*args, **kwargs)
checker = inspect.signature(function).return_annotation
if not check('return', result, checker, function):
raise Exception(['return'])
# return the result.
return result
return wrapper
def check(name, value, checker, function):
if isinstance(checker, (tuple, list, set)):
return True in [check(name, value, sub_checker, function) for sub_checker in checker]
elif checker is inspect._empty:
return True
elif checker is None:
return value is None
elif isinstance(checker, type):
return isinstance(value, checker)
elif callable(checker):
result = checker(value)
return result
def testcases():
@auto_type_checker
def add(a, b, c: (int, float), d: int) -> str:
return a + b + c + d
@auto_type_checker
def test(x: lambda x: x >= 0, y: list, z: str) -> str:
return y[x] == z
class Test():
base = 1
@auto_type_checker
def add(self, a, b, c: [int, float], d: int) -> lambda x: str(x):
return self.base + a + b + c + d
try:
print(add(1, 2, 2.3, 3.4))
except Exception as e:
print(e)
try:
print(test(-2, 3, 4))
except Exception as e:
print(e)
try:
print(test(2, [1, 2, 3, 4], '3'))
except Exception as e:
print(e)
var = Test()
try:
var.add(1, 2, 3.3, 4.5)
except Exception as e:
print(e)
try:
var.add(1, 2, 3, 4)
except Exception as e:
print(e)
if __name__ == '__main__':
testcases()
其中,auto_type_checker 是一个修饰器,在函数定义的时候调用即可。函数在声明的时候,如果需要进行入参合法性校验的话,就用如下语法为函数的输入输出指定 checker。
@auto_type_checker
def function(arg1: int, arg2, arg3: (int, float) = 0, arg4: lambda x: x > 0 = 1) -> list:
return [arg1, arg2, arg3, arg4]
上述代码有 4 种 checker:不指定 checker,如 arg2,auto_type_checker 不会为 arg2 进行合法性校验;
type 型 checker,如 arg1,auto_type_checker 会检测 arg1 是否是 int 型,如果不是,会抛出异常,而返回值必须是 list 型,否则也会抛出异常;
函数型 checker,如 arg4,auto_type_checker 会将 arg4 带入到 checker,如果 checker 的返回值是 Fasle,则抛出异常,上述代码中,arg4 只接受大于 0 的数字;
tuple/list 型 checker,如 arg3,tuple 或 list 中的所有元素都会被当作 checker,当所有 checker 都无法通过校验,则抛出异常,上述代码中,arg3 允许整数或浮点数。
这样声明函数,一是利用 PEP 3107 之 Function Annotations 特性为函数入参出参添加注释,调用者可以知道应该往函数里传入什么变量,另外只需要添加一行 @auto_type_checker 便可以实现入参出参的自动检测,非常的 pythonic。