Python 类型注解和参数类型检查
1、类型注解
1.1 函数定义的弊端
Python 是动态语言,变量随时可以被赋值,且能赋值为不同的类型。
Python 不是静态编译型语言,变量类型是在运行期决定的。
动态语言很灵活,但是这种特性也是弊端。
实例:
def add(x, y): return x + y print(add(4, 5), add('4', '5'), add([4], [5]), add((4,), (5,))) # 太灵活
难发现: 由于不做任何类型检查,直到运行期问题才显现出来,或者线上运行时才能暴露出问题。
难使用: 函数的使用者看到函数的时候,并不知道你函数的设计,并不知道应该传入什么类。
1.2 弊端如何解决
1.2.1 增加文档
Documentation String,这只是一个惯例,不是强制标准,不能要求程序员一定为函数提供说明文档。如果提供了说明文档,函数定义更新了,文档未必同步更新也是一个问题。
实例:
def add(x, y): """ x : int y : int return int """ return x + y print(help(add)) # help 查看文档
1.2.2 函数注解
Function Annotations,Python 3.5 引入,对函数的参数进行类型注解、对函数的返回值进行类型注解;
只对函数参数做一个辅助的说明,并不对函数参数进行类型检查;
提供给第三方工具,做代码分析,发现隐藏的 bug;
函数注解的信息,保存在 __annotations__
属性中。
实例:
def add(x:int, y:int) -> int: return x + y print(add.__annotations__) # 输出结果: {'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
1.2.3 变量注解
Python 3.6 引入,注意它也只是一种对变量的说明,非强制。
实例:
i:int = 3
2、参数类型检查
2.1 思路
函数参数的检查,一定是在函数外。如要把检查代码侵入到函数中,函数应该作为参数,传入到检查函数中,检查函数拿到函数传入的实际参数,与形参声明对比(柯里化 — 装饰器)
__annotations__
属性是一个字典,其中包括返回值类型的声明。假设要做位置参数的判断,无法和字典中的声明对应,使用 inspect 模块。
2.2 inspect 模块
提供获取对象信息的函数,可以检查函数和类、类型检查。
实例:
print(inspect.isbuiltin(int)) # 是否是内建对象:False print(inspect.isfunction(int)) # 是否是函数:False print(inspect.isclass(int)) # 是否是类:True print(inspect.ismethod(int)) # 是否是类的方法:False print(inspect.isgenerator((i for i in range(5)))) # 是否是生成器对象:True def fib(n): yield from range(10) print(inspect.isgeneratorfunction(fib)) # 是否是生成器函数:True print(inspect.ismodule(inspect)) # 是否是模块:True ...... 还有很多 is 函数,需要的时候查阅 inspect 模块帮助
2.2.1 signature 方法
inspect.signature(callable) 获取签名(函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息)
实例:
import inspect def add(x: int, y: int) -> int: return x + y print(type(inspect.signature(add)), inspect.signature(add)) # 输出结果: <class 'inspect.Signature'> (x: int, y: int) -> int
2.2.2 Parameter 对象
保存在元组中,是只读的。
name,参数的名字
annotation,参数的注解,可能没有定义
default,参数的缺省值,可能没有定义
empty,特殊的类,用来标记default属性或者注释annotation属性的空值
kind,实参如何绑定到形参,就是形参的类型:
POSITIONAL_ONLY,值必须是位置参数提供
POSITIONAL_OR_KEYWORD,值可以作为关键字或者位置参数提供
VAR_POSITIONAL,可变位置参数,对应
*args
KEYWORD_ONLY,keyword-only 参数,对应
*
或者*args
之后的出现的非可变关键宇参数
VAR_KEYWORD,可变关键字参数,对应
**kwargs
实例:
import inspect # 检查模块 def add(x, y:int=7, *args, z, t=10, **kwargs) -> int: return x + y sig = inspect.signature(add) # 获取签名 print(sig) # 签名:参数类型即返回类型信息 print() print('params:', sig.parameters) # 有序字典:参数信息 print() print('return:', sig.return_annotation) # 返回值 # 输出结果: ''' (x, y: int = 7, *args, z, t=10, **kwargs) -> int params: OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y: int = 7">), ('args', <Parameter "*args">), ('z', <Parameter "z">), ('t', <Parameter "t=10">), ('kwargs', <Parameter "**kwargs">)]) return: <class 'int'> '''
import inspect # 检查模块 def add(x, y:int=7, *args, z, t=10, **kwargs) -> int: return x + y sig = inspect.signature(add) # 获取签名 for i, item in enumerate(sig.parameters.items()): name, param = item print(i, name, param.annotation, param.kind, param.default) print(param.default is param.empty, end='\n\n') # 输出结果: ''' 0 x <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'inspect._empty'> True 1 y <class 'int'> POSITIONAL_OR_KEYWORD 7 False 2 args <class 'inspect._empty'> VAR_POSITIONAL <class 'inspect._empty'> True 3 z <class 'inspect._empty'> KEYWORD_ONLY <class 'inspect._empty'> True 4 t <class 'inspect._empty'> KEYWORD_ONLY 10 False 5 kwargs <class 'inspect._empty'> VAR_KEYWORD <class 'inspect._empty'> True '''
2.3 业务应用
有函数如下:
def add(x, y:int=7) -> int: return x + y
请检查用户输入是否符合参数注解的要求。
思路:
调用时,用户传入实参,才能判断用户输入的实参是否符合要求。
调用时,用户感觉上还是在调用 add 函数。
对用户输入的数据和声明的类型进行对比,如果不符合,提示用户。
# -*- coding:utf-8 -*- # version:python3.7 import inspect # 检查模块 def add(x, y:int=7) -> int: return x + y def check(fn): def wrapper(*args, **kwargs): sig = inspect.signature(fn) # 获取签名:(x, y: int = 7) -> int params = sig.parameters # 获取参数:有序字典 #OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y: int = 7">)]) values = list(params.values()) # 获取values #print(values, type(values[0])) #[<Parameter "x">, <Parameter "y: int = 7">] <class 'inspect.Parameter'> keys = list(params.keys()) for i, x in enumerate(args): # 遍历位置参数 if values[i].annotation != inspect._empty and not isinstance(x, values[i].annotation): raise TypeError('Wrong param {} {}'.format(keys[i], x)) # for x, (k, v) in zip(args, params.items()): # 遍历位置参数 # if v.annotation != inspect._empty and not isinstance(x, v.annotation): # raise TypeError('Wrong param {} {}'.format(k, v)) for k, v in kwargs.items(): # 遍历关键字参数 if params[k].annotation != inspect._empty and not isinstance(v, params[k].annotation): raise TypeError('Wrong param {} {}'.format(k, v)) ret = fn(*args, **kwargs) return ret return wrapper print(check(add)(4, y='5')) # 输出结果: raise TypeError('Wrong param {} {}'.format(k, v)) TypeError: Wrong param y 5
柯里化:
# -*- coding:utf-8 -*- # version:python3.7 import inspect # 检查模块 def check(fn): def wrapper(*args, **kwargs): sig = inspect.signature(fn) # 获取签名:(x, y: int = 7) -> int params = sig.parameters # 获取参数:有序字典 #OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y: int = 7">)]) values = list(params.values()) # 获取values #print(values, type(values[0])) #[<Parameter "x">, <Parameter "y: int = 7">] <class 'inspect.Parameter'> keys = list(params.keys()) for i, x in enumerate(args): # 遍历位置参数 if values[i].annotation != inspect._empty and not isinstance(x, values[i].annotation): raise TypeError('Wrong param {} {}'.format(keys[i], x)) # for x, (k, v) in zip(args, params.items()): # 遍历位置参数 # if v.annotation != inspect._empty and not isinstance(x, v.annotation): # raise TypeError('Wrong param {} {}'.format(k, v)) for k, v in kwargs.items(): # 遍历关键字参数 if params[k].annotation != inspect._empty and not isinstance(v, params[k].annotation): raise TypeError('Wrong param {} {}'.format(k, v)) ret = fn(*args, **kwargs) return ret return wrapper @check def add(x, y:int=7) -> int: return x + y print(add(4, 5))
可以通过遍历传入的参数是不是类型注解的类型,来进行参数类型检查。如果不是,则通过raise触发异常或者记录到日志并不触发异常(保证服务的运行)。