引言
Python作为一门动态类型语言,有时候,一个不小心的类型错误只有在实际运行中才有可能被发现。相较而言,静态类型语音虽然不够灵活,但是,类型错误等语法检查在编译期间就可以提前发现了。
那么我们有没有方法在Python代码中也添加类型检查的功能呢?本文就通过装饰器简单实现函数参数类型检查的功能,从而在实战中,进一步加深对装饰器的理解。
本文的主要内容有:
1、Python中的类型注解
2、函数签名获取
3、装饰器实现类型检查
Python中的类型注解
Python是动态类型语言,定义一个变量时,不需要显式指定其类型,但是,这并不意味着不进行类型检查,虽然更多的是运行时进行必要的检查(是否有对应属性、方法的检查)。
在静态类型语言中,定义一个变靓一定要指定变量的类型,比如C语言:
尝试对整型变量a赋值一个浮点数3.14,编译时就会报错了。
动态类型语言,很多时候都是没有编译环节的,所以,类型的错误直到运行时才能被发现。
此外,由于Python中不需要(也不能)显式指定变量的类型,很多时候,对模块的使用方其实是有一定的使用成本的。
Python3.5引入了一个特性:类型注解(Type Annotations),主要用于静态类型检查。
类型注解允许在函数定义中标注参数和返回值的类型,从而使得代码更加清晰、可读。
以实际代码,简单看下类型注解的基本语法:
1、函数定义中的每个形参后面追加: 类型,来显式提醒形参的数据类型。
2、形参列表之后,以 -> 类型,来提醒函数返回值的数据类型。
3、通过函数对象的属性/元数据:__annotations__可以获取到函数定义中的类型注解信息。
执行结果:
如果有复杂的类型,可以通过typing模块导入,比如:
需要说明的是,类型注解只起到了提醒的作用,IDE中也能做到类型不兼容的提醒。但是,即使不遵循类型注解的要求,代码还是可以正常执行的。
所以,虽然不存在语法上的强制要求或者类型检查,类型注解的使用场景,在于当涉及到团队协作,公共模块的开发时,可以通过类型注解以及docstring等增加代码的可读性。
函数签名获取
在我们尝试对Python中的函数调用进行类型检查之前,还有一个前置条件需要解决,就是,我们如何获取到检查函数类型所需要的相关元数据信息,主要是函数的签名信息。
简单介绍一下函数签名的概念。
所谓的函数签名(Function Signature)是指描述了函数的名称、参数(包括参数类型和默认值)以及返回值类型的完整信息。函数签名是关于函数如何调用的完整描述。有助于开发者更好地理解函数的使用方式,并在代码维护和文档编写中能起到很重要的作用。
在Python中如何获取函数的签名信息呢,我们可以尝试之前在简单介绍过的inspect模块。
需要说明的是,本文主要聚焦于通过inspect模块获取函数的签名信息,关于inspect模块更多的内容,后面可以以专题文章来介绍。
直接看代码:
import inspect
# 为了更清楚的查看参数签名,让函数定义更加负责(奇怪)
def add(a: int, b: int = 10, *, c: int = 20) -> int:
return a + b + c
if __name__ == '__main__':
sig = inspect.signature(add)
print(f"函数签名对象的类型:{type(sig)}")
print(f"函数签名:{sig}")
# 通过签名对象的parameters属性获取参数信息
for name, param in sig.parameters.items():
print(f"参数名:{name},参数信息:{param}")
print(f"\t 参数类型:{param.kind}")
print(f"\t 参数默认值:{param.default}")
print(f"\t 参数注解:{param.annotation}")
# 也可以获取函数的返回值注解
print(f"函数返回值注解:{sig.return_annotation}")
执行结果:
其实,很多时候,函数定义中的类型注解,可能是不写的,所以,我们要做函数类型检查时,也不能依赖函数的类型注解。
我们之所以通过inspect模块获取到函数的签名,更多的是想要获取到函数的参数列表(有序的),基于这个参数列表,再结合带参数的装饰器,就可以实现函数调用的类型检查了。
装饰器实现类型检查
要想实现函数类型的检查,我们还需要实现将函数的参数列表与函数调用要求的类型进行一一映射绑定的操作,这一点可以通过函数签名对象的bind()或者bind_partial()函数,首先看下这两个函数的定义:
关于_bind()函数的定义,可以自行查看,这里就不展开了。
简单描述一下,进行类型检查的实现步骤:
1、定义一个带参数的装饰器,通过参数,可以动态指定参数调用的数据类型,允许只有部分参数需要进行类型检查。
2、在装饰器函数中,通过inspect模块获取要装饰的原始函数的函数签名。
3、通过函数签名对象的bind_partial()函数,将装饰器的参数与函数签名对象的参数列表进行绑定,形成一一映射关系。
4、在装饰器的内部嵌套函数中(也就是装饰器最终返回的函数对象),将函数的实际调用参数与函数的签名对象的参数列表也进行绑定。
5、将实际调用参数的类型,与前面获取的类型绑定 ,进行一一比较。
概括来说,就是以函数签名为媒介,将函数参数的类型要求,与实际调用传递的函数参数的值进行一一对应,然后进行类型的检查操作。
直接看代码示例:
import inspect
def type_check(*type_args, **type_kwargs):
def decorator(func):
sig = inspect.signature(func)
arg_types = sig.bind_partial(*type_args, **type_kwargs).arguments
def wrap(*args, **kwargs):
call_args = sig.bind(*args, **kwargs).arguments
for name, parma in call_args.items():
arg_type = arg_types.get(name)
if arg_type:
if not isinstance(parma, arg_type):
raise TypeError(f'参数{name}必须是{arg_type}类型')
return func(*args, **kwargs)
return wrap
return decorator
@type_check(int, int)
def add(a, b):
return a + b
# 也可以只对形参a进行类型检查
@type_check(a=int)
def multiply(a, b):
return a * b
if __name__ == '__main__':
print(add(10, 20))
print(multiply(10, 10.5))
print('=' * 20)
print(add(10, 20.5))
# multiply(10.5, 10)
执行结果:
当然,参考类似的思路,对函数参数的取值范围,也是可以通过inspect结合装饰器来动态添加检查机制的,这里就不再列举了,感兴趣的同学可以自行尝试。
总结
本文分别介绍了Python中类型注解的特性、函数签名的概念,以及通过inspect获取函数签名信息、通过bind()、bind_partial()实现对函数签名进行参数的绑定。最终通过一个完整的类型检查的实现代码,展示了通过装饰器实现动态语言的动态类型检查的实现。
感谢您的拨冗阅读,希望对您有所帮助。