一、函数定义的弊端
1.1 动态语言很灵活,但是这种特性也是弊端
Python是动态语言,变量随时可以被赋值,且能赋值为不同的类型。Python不是静态编译型语言,变量类型是在运行器决定的。
动态语言很灵活,但是这种特性也是弊端,如下面的函数
def add(x, y):
return x + y
print(add(4, 5))
print(add('hello', 'world'))
add(4, 'hello')
难发现:由于不做任何类型检查,直到运行问题才显现出来,或者线上运行时才能暴露出问题
难使用:函数的使用者看到函数的时候,并不知道你的函数的设计,并不知道应该传入什么类型的数据
如何解决这种动态语言定义的弊端
1.2 增加文档字符串
增加 Documentation String 只是一个惯例,不是强制标准,不能要求程序员一定为函数提供说明文档。但是函数定义更新了,文档未必同步更新。
def add(x, y):
'''
:param x: int
:param y: int
:return: int
'''
return x + y
print(help(add))
#以上代码执行结果如下:
Help on function add in module __main__:
add(x, y)
:param x: int
:param y: int
:return: int
None
1.3 函数注解
def add(x:int , y:int) -> int :
'''
:param x: int
:param y: int
:return: int
'''
return x + y
print(help(add))
print(add(4, 5))
print(add('jacky', 'fan'))
#以上代码执行结果如下:
Help on function add in module __main__:
add(x:int, y:int) -> int
:param x: int
:param y: int
:return: int
None
9
jackyfan
二、函数注解Function Annotations
2.1 函数注解
- Python 3.5引入
- 对函数的参数进行类型注解
- 对函数的返回值进行类型注解
- 只对函数参数做一个辅助的说明,并不对函数参数进行类型检查
- 提供给第三方工具,做代码分析,发现隐藏的bug
- 函数注解的信息,保存在
__annotations__
属性中
def add(x:int , y:int) -> int :
pass
2.2 变量注解
Python 3.6引入,也只是对变量的一种说明,非强制
i:int = 3
三、业务应用 -- 函数参数类型检查
3.1 思路
- 函数参数的检查,一定是在函数外
- 函数应该作为参数,传入到检查函数中
- 检查函数拿到函数传入的实际参数,与形参声明对比
-
__annotations__
属性是一个字典,其中包括返回值类型的声明。 - 假设要做位置参数的判断,无法和字典中的声明对应。这是要使用inspect模块
3.2 inspect模块
提供获取对象信息的函数,可以检查函数和类、类型检查
3.2.1 inspect的signature函数
signature(callable)
获取签名(函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息)
def add(x:int, y:int, *args,**kwargs) -> int:
return x + y
print(add.__annotations__) #不推荐使用它,返回的结果是无序的,无法做位置参数的类型判断;而且无法获取args和kwargs
print('*' * 20)
sig = inspect.signature(add) # 生成函数签名对象
print(sig, type(sig), sep='\n')
print('params : ', sig.parameters) # OrderedDict, 即返回的结果是一个有序字典(参数相关的字典)
print('return : ', sig.return_annotation) # 返回值的类型注解
print(sig.parameters['y'], type(sig.parameters['y'])) # OrderedDict中的键是参数名,键值是,包含了参数的相关信息
print(sig.parameters['x'].annotation)
print(sig.parameters['args'])
print(sig.parameters['args'].annotation)
print(sig.parameters['kwargs'])
print(sig.parameters['kwargs'].annotation)
#以上代码执行结果如下:
{'x': , 'y': , 'return': }
**************************************************************************************
(x:int, y:int, *args, **kwargs) -> int
params : OrderedDict([('x', ), ('y', ), ('args', ), ('kwargs', )])
return :
y:int
*args
**kwargs
sig.parameters
返回的是和参数相关的有序字典,其中的键是参数名,键值是
Parameter对象:
保存在元组中,是只读的
- name,参数的名字
- annotation,参数的注解,可能没有定义
- default,参数的缺省值,可能没有定义
- empty,特殊的类,用来标记default属性或者注释annotation属性的空值(即当default属性和annotation属性为空时,值显示为,而不是None,''之类的)
- kind,实参如何绑定到形参,就是形参的类型:
POSITIONAL_ONLY,值必须是位置参数提供
POSITIONAL_OR_KEYWORD,值可以作为关键字或者位置参数提供
VAR_POSITIONAL,可变位置参数,对应*args
KEYWORD_ONLY,keyword-only参数,对应*或者*args之后的出现的非可变关键字参数
VAR_KEYWORD,可变关键字参数,对应**kwargs
举例:
def add(x, y:int=7, *args, z, t=10,**kwargs) -> int:
return x + y
sig = inspect.signature(add)
print(sig)
print('params : ', sig.parameters) # 有序字典
print('return : ', sig.return_annotation)
print("*" * 20 + "我是分割线" + "*" * 20)
for i, item in enumerate(sig.parameters.items()):
name, param = item
print(i+1, name, param.annotation, param.kind, param.default)
print(param.default is param.empty, end='\n\n') # 判断参数是否有默认值
#以上代码执行结果如下:
(x, y:int=7, *args, z, t=10, **kwargs) -> int
params : OrderedDict([('x', ), ('y', ), ('args', ), ('z', ), ('t', ), ('kwargs', )])
return :
********************我是分割线********************
1 x POSITIONAL_OR_KEYWORD
True
2 y POSITIONAL_OR_KEYWORD 7
False
3 args VAR_POSITIONAL
True
4 z KEYWORD_ONLY
True
5 t KEYWORD_ONLY 10
False
6 kwargs VAR_KEYWORD
True
3.2.2 inspect的其他方法
是否是函数:inspect.isfunction(add)
是否是类的方法:inspect.ismethod(add)
是否是生成器对象:inspect.isgenerator(add)
是否是生成器函数:inspect.isgeneratorfunction(add)
是否是类:inspect.isclass(add)
是否是模块:inspect.ismodule(inspect)
是否是内建对象:inspect.isbuiltin(print)
#还有很多is函数,需要的时候查阅inspect模块帮助
3.3 小试牛刀
有函数如下:
def add(x, y:int=7) -> int:
return x + y
请检查用户输入是否符合参数注解的要求?
思路:
- 调用时,判断用户输入的实参是否符合要求
- 调用时,用户感觉上还是在调用add函数
- 对用户输入的数据和声明的类型进行对比,如果不符合,提示用户
import inspect
def check(fn):
def wrapper(*args, **kwargs):
sig = inspect.signature(fn)
params = sig.parameters
values = list(params.values())
# 对位置传参进行类型判断
for i,p in enumerate(args):
# inspect.signature.parameters返回的有序字典,就是按照位置参数的顺序排的,所以位置实参的索引和有序字典的键值列表的索引是一致的
param = values[i]
# 先判断该参数是否有类型注解,有的话就检查实参类型是否符合类型注解要求,没有就不检查
if param.annotation is not param.empty and not isinstance(p, param.annotation):
print(p, '!=', values[i].annotation)
# 对关键字传参进行类型判断
for k,v in kwargs.items():
param = params[k]
# 先判断该参数是否有类型注解,有的话就检查实参类型是否符合类型注解要求,没有就不检查;inspect._empty 和 param.empty 是一样的
if param.annotation is not inspect._empty and not isinstance(v, param.annotation):
print(k, v, '!=', param.annotation)
return fn(*args, **kwargs)
return wrapper
@check
def add(x, y:int=7) -> int: #要求第二个参数必须是int类型,并且返回值类型也为int
return x + y
print(add(2,1))
print(add(20, y=10))
print(add(y=100, x=200))
print(add('jacky', 'fan')) #位置传参时故意不按照要求传参,发现会有相应的提示信息
print(add('jacky', y='fan')) #关键字传参时故意不按照要求传参,发现会有相应的提示信息
#以上代码执行结果如下:
3
30
300
fan != #位置传参的类型错误能够捕捉
jackyfan
y fan != #关键字传参的类型错误能够捕捉
jackyfan