参数注解

一、函数定义的弊端

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中的键是参数名,键值是<class 'inspect.Parameter'>,包含了参数的相关信息
print(sig.parameters['x'].annotation)
print(sig.parameters['args'])
print(sig.parameters['args'].annotation)
print(sig.parameters['kwargs'])
print(sig.parameters['kwargs'].annotation)





#以上代码执行结果如下:
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
**************************************************************************************
(x:int, y:int, *args, **kwargs) -> int 
<class 'inspect.Signature'>
params :  OrderedDict([('x', <Parameter "x:int">), ('y', <Parameter "y:int">), ('args', <Parameter "*args">), ('kwargs', <Parameter "**kwargs">)])
return :  <class 'int'>
y:int <class 'inspect.Parameter'>
<class 'int'>
*args
<class 'inspect._empty'>
**kwargs
<class 'inspect._empty'>

sig.parameters 返回的是和参数相关的有序字典,其中的键是参数名,键值是<class 'inspect.Parameter'>,包含了参数的相关信息:

Parameter对象:
    保存在元组中,是只读的
    - name,参数的名字
    - annotation,参数的注解,可能没有定义
    - default,参数的缺省值,可能没有定义
    - empty,特殊的类,用来标记default属性或者注释annotation属性的空值(即当default属性和annotation属性为空时,值显示为<class 'inspect._empty'>,而不是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', <Parameter "x">), ('y', <Parameter "y:int=7">), ('args', <Parameter "*args">), ('z', <Parameter "z">), ('t', <Parameter "t=10">), ('kwargs', <Parameter "**kwargs">)])
return :  <class 'int'>
********************我是分割线********************
1 x <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'inspect._empty'>
True

2 y <class 'int'> POSITIONAL_OR_KEYWORD 7
False

3 args <class 'inspect._empty'> VAR_POSITIONAL <class 'inspect._empty'>
True

4 z <class 'inspect._empty'> KEYWORD_ONLY <class 'inspect._empty'>
True

5 t <class 'inspect._empty'> KEYWORD_ONLY 10
False

6 kwargs <class 'inspect._empty'> VAR_KEYWORD <class 'inspect._empty'>
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 != <class 'int'>                #位置传参的类型错误能够捕捉
jackyfan
y fan != <class 'int'>              #关键字传参的类型错误能够捕捉
jackyfan
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容