python类型注解及functools中的其他方法

  • python函数定义的弊端
  • 函数注解:Function Annotations
  • 函数注解的作用
  • 业务应用
  • inspect模块
  • is判断系列
  • 返回的parameters对象
  • 代码示例
  • 将获取数据的功能改造成装饰器
  • functools中的其他方法
  • partial 方法
  • lru_cache方法
  • 斐波那契数列的改造
  • lru_cache方法的应用
  • 缓存的简单介绍


python函数定义的弊端

  • python是动态语言,变量随时可以被复制后,且能够赋值为不同的类型
  • python不是静态编译型语言,变量类型是在运行器上决定的
  • 动态语言很灵活,但是这种特性也是弊端
  • 弊端一:难发现。由于不做任何类型的检查,直到运行问题才会显示出现,或者线上运行时才会爆喽问题
  • 弊端二:难使用,函数的使用者看到函数的时候,并不知道你函数的设计,并不知道应该传入什么类型的数据

函数注解:Function Annotations

  • 增加文档:Documentation String
  • 这只是一个惯例,并不是强制的标准,不能要求程序员员一定为函数提供说明文档
  • 函数定义更新了,文档未必同步更新

函数注解的作用

  • python 3.5开始引入
  • 对函数的返回值进行类型注解
  • 对函数的参数进行类型注解
  • 只对函数参数做一个辅助说明,并不对函数参数进行类型检查
  • 提供给第三方工具,做代码分析,发现隐藏的bug
  • 函数注解的信息,保存在__annotations__属性中
  • 变量注解从python3.6开始引入
  • 下方代码中,x:int,y:int告诉使用者,x和y应传int类型,->int的含义是:如果传参类型正确,返回的也是int类型
  • i:int = 3是变量注解,告知i是一个int类型
def add(x:int,y:int)->int:
    '''
    用来写函数的作用
    :param x:
    :param y:
    :return:
    '''
    i:int = 3
    return x+y+i
print(add(4,5))
print("hello","word")
print(add.__annotations__)

=======run_result============
12
hello word
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}

业务应用

  • 函数参数类型的检查
  • 思路:
  • 函数参数的检查,一定在函数外
  • 函数应该作为参数,传入到检查函数中
  • 检查函数拿到函数传入的实际参数,与形参声明对比
  • __annotations__属性是一个字典,其中包括返回值类型的声明,假设要做位置参数的判断,无法和字典中的声明做队医你过,使用inspect模块
  • inspect模块:提供获取对象的函数,可以检查函数和类,类型检查

inspect模块

  • inspect.signature(callable) 1.获取签名(函数签名包含一个函数的信息,包括函数名 参数类型,他所在的类和名称空间及其他信息)
    2.add.__annotations__也能返回函数的属性,这里不用这个属性的原因是:__annotations__的返回值并不是有序字典,位置不固定,只能拿名称对应,不能拿顺序对应

is判断系列

  • inspect.isfunction(add) 是否是函数
  • inspect.ismethod(add) 是否是类的方法
  • inspect.isgeneratorfunction(add) 是否是生成器函数
  • inspect.isgenerator(add) 是和否是生成器对象
  • inspect.isclass(add) 是否是类
  • inspect.ismodule(add)是否是模块
  • inspect.isbuiltin(add) 是否是内建对象
  • 还有其他的一些类型,python定义的,都可以在这儿找到,请仔细ing学习

返回的parameters对象

  • 保存在元组中,是只读的
  • name,表示参数的名字
  • annotation,表示参数的注解
  • default,参数的缺省值,可能没有定义
  • empty,特殊的类,用来标记default属性,或者人注释annotation属性的空值
  • kind,实参如何绑定形参,就是形参的类型
    1.POSITIONAL_OR_KEYWORD,含义是值作为默认参数或者位置参数提供
    2.VAR_POSITIONAL,含义是动态参数,对应*args
    3.VAR_KEYWORD,含义是关键字参数,对应**kwargs 4.KEYWORD_ONLY,含义是keyword-only参数,对应*或者*args之后出现的非关键字参数
    5.POSITIONAL_ONLY,含义是值必须是位置参数提供(不常见,可能就没有实现,见到了知道是什么就好)

代码示例

def add(x:int,y:int,z:int=9,*args,c,**kwargs)->int:
    '''
    用来写函数的作用
    :param x:
    :param y:
    :return:
    '''
    i:int = 3
    return x+y+i+z

import inspect
print(add.__annotations__)
flag= inspect.signature(add)#函数签名
print("***********inspect签名*****************")
print("params的信息:",flag.parameters)#OrderedDict,有序字典
print("return的信息:",flag.return_annotation)
print("x的信息和y的属性:",flag.parameters["x"],flag.parameters["y"].annotation)
print("args的信息和属性:",flag.parameters["args"],flag.parameters["args"].annotation)
print("kwargs的信息和属性:",flag.parameters["kwargs"],flag.parameters["kwargs"].annotation)
print("************is判断系列****************")
print("是否是函数:",inspect.isfunction(add))
print("是否是类的方法:",inspect.ismethod(add))
print("是否是生成器函数:",inspect.isgeneratorfunction(add))
print("是否是生成器对象:",inspect.isgenerator(add))
print("是否是类:",inspect.isclass(add))
print("是否是模块:",inspect.ismodule(add))
print("是否是内建对象:",inspect.isbuiltin(add))
print("************函数返回的parameters对象****************")
print("参数名称:",flag.parameters["x"].name)
print("参数注释:",flag.parameters["x"].annotation)
print("参数缺省值:",flag.parameters["x"].default)
print("参数缺省值:",flag.parameters["z"].default)
print("特殊的类,标记default的属性:",flag.parameters["x"].empty)

print("形参x的类型:",flag.parameters["x"].kind)
print("形参z的类型:",flag.parameters["z"].kind)
print("形参*args的类型:",flag.parameters["args"].kind)
print("形参**kwargs的类型:",flag.parameters["kwargs"].kind)
print("形参c的类型:",flag.parameters["c"].kind)

============run_result===============
{'x': <class 'int'>, 'y': <class 'int'>, 'z': <class 'int'>, 'return': <class 'int'>}
(x: int, y: int, z: int = 9, *args, c, **kwargs) -> int
************inspect签名****************
params的信息: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">), ('z', <Parameter "z: int = 9">), ('args', <Parameter "*args">), ('c', <Parameter "c">), ('kwargs', <Parameter "**kwargs">)])
return的信息: <class 'int'>
x的信息和y的属性: x: int <class 'int'>
args的信息和属性: *args <class 'inspect._empty'>
kwargs的信息和属性: **kwargs <class 'inspect._empty'>
************is判断系列****************
是否是函数: True
是否是类的方法: False
是否是生成器函数: False
是否是生成器对象: False
是否是类: False
是否是模块: False
是否是内建对象: False
************函数返回的parameters对象****************
参数名称: x
参数注释: <class 'int'>
参数缺省值: <class 'inspect._empty'>
参数缺省值: 9
特殊的类,标记default的属性: <class 'inspect._empty'>
形参x的类型: POSITIONAL_OR_KEYWORD
形参z的类型: POSITIONAL_OR_KEYWORD
形参*args的类型: VAR_POSITIONAL
形参**kwargs的类型: VAR_KEYWORD
形参c的类型: KEYWORD_ONLY

将获取数据的功能改造成装饰器

import functools
import inspect
def logger(fn):
    @functools.wraps(fn)
    def logger_1(*args,**kwargs):
        #实参检查
        print(args,kwargs)#args前可以加*,kwargs前不能加*
        #原因:kwargs参数是关键字参数,必须要带变量名,函数中默认值的变量没有定义,他会找不到的
        flag= inspect.signature(fn)
        print("标签:",flag)
        print("param:",flag.parameters)
        print("return:",flag.return_annotation)
        print("****************分界线****************")
        #flag.parameters最终解析的结果是个字典,因此可以使用item方法,
        # 因为enumerate返回了索引,因此需要三个变量接收
        for i ,(name,value) in enumerate(flag.parameters.items()):
            print(i+1,name,value.annotation,value.kind,value.default)
            print(value.default is value.empty,end="\n\n")

        ret = fn(*args,**kwargs)
        return ret
    return  logger_1

@logger
def add(x:int,y:int,z:int=9,*args,c,**kwargs)->int:
    '''
    用来写函数的作用
    :param x:
    :param y:
    :return:
    '''
    i:int = 3
    return x+y+i+z

k = add(4,y=7,c=10)

=============run_result===============
(4,) {'y': 7, 'c': 10}
标签: (x: int, y: int, z: int = 9, *args, c, **kwargs) -> int
param: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">), ('z', <Parameter "z: int = 9">), ('args', <Parameter "*args">), ('c', <Parameter "c">), ('kwargs', <Parameter "**kwargs">)])
return: <class 'int'>
****************分界线****************
1 x <class 'int'> POSITIONAL_OR_KEYWORD <class 'inspect._empty'>
True

2 y <class 'int'> POSITIONAL_OR_KEYWORD <class 'inspect._empty'>
True

3 z <class 'int'> POSITIONAL_OR_KEYWORD 9
False

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

5 c <class 'inspect._empty'> KEYWORD_ONLY <class 'inspect._empty'>
True

6 kwargs <class 'inspect._empty'> VAR_KEYWORD <class 'inspect._empty'>
True
  • 客户不需要知道中间校验的判断结果,因此控制台中输出的信息需要控制,不要将校验的结果输出在控制台上
  • 代码优化如下:
import inspect
def logger(fn):
    def logger_1(*args,**kwargs):
        #实参检查
        print(args,kwargs)#args前可以加*,kwargs前不能加*
        #原因:kwargs参数是关键字参数,必须要带变量名,函数中默认值的变量没有定义,他会找不到的
        flag= inspect.signature(fn)
        param = flag.parameters
        print(param)
        print("****************分界线****************")
        #关键字传参验证
        for k,v in kwargs.items():
            if isinstance(v,param[k].annotation):
                print(v,"==", param[k].annotation,"result is True")
            else:
                print(v, "==", param[k].annotation, "result is False")
        #位置参数验证
        param_list = tuple(param.keys())#将有序字典的key放到列表中
        for x,y in enumerate(args):
            key = param_list[x]
            if isinstance(y,param[key].annotation):
                print(y, "==", param[key].annotation, "result is True")
            else:
                print(y, "==", param[key].annotation, "result is False")
        ret = fn(*args,**kwargs)
        return ret
    return  logger_1

@logger
def add(x:int,y:int,z:int=9,*args,c,**kwargs)->int:
    '''
    用来写函数的作用
    :param x:
    :param y:
    :return:
    '''
    i:int = 3
    return x+y+i+z

print("run_add_result:",add(4,7,c=10,z=99))
print("name={}\ndoc={}".format(add.__name__,add.__doc__))
================run_result==============
(4, 7) {'c': 10, 'z': 99}
OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">), ('z', <Parameter "z: int = 9">), ('args', <Parameter "*args">), ('c', <Parameter "c">), ('kwargs', <Parameter "**kwargs">)])
****************分界线****************
10 == <class 'inspect._empty'> result is False
99 == <class 'int'> result is True
4 == <class 'int'> result is True
7 == <class 'int'> result is True

run_add_result: 113
name=add
doc=用来写函数的作用
    :param x:
    :param y:
    :return:

functools中的其他方法

partial 方法
  • 偏函数,将部分函数的参数固定下来,相当于为部分函数的参数添加了一个固定的默认值,形成一个新的函数并返回
  • 从partial生成的新函数,是对原装函数的封装,从下方的代码中可以看出,对函数使用partial方法之后,生成的newadd为新函数,和add函数不是一个,方法的内存地址已变
  • 下方代码显示,给key-word的变量c的值,已经写入函数的默认值中了;使用关键字参数传参,可以改变偏函数中设置函数的参数信息
  • 代码示例:
import functools

def add(x:int,y:int,z:int=9,*args,c,**kwargs):
    print(x,y,z,c)
    print(args)
    print(kwargs)
    print("*********分割线************")

newadd = functools.partial(add,c=0)
print(newadd(7,8,c=10))
print(newadd(1,2,3,4,5,6,7,8))

import inspect
print(inspect.signature(newadd))

================run_result============
7 8 9 10
()
{}
*********分割线************
None
1 2 3 0
(4, 5, 6, 7, 8)
{}
*********分割线************
None
(x: int, y: int, z: int = 9, *args, c=0, **kwargs)
  • 如果使用位置代码传参,偏函数固定的参数建议不要是位置参数,因为会报参数重复,代码如下:
import functools

def add(x:int,y:int,z:int=9,*args,c,**kwargs):
    print(x,y,z,c)
    print(args)
    print(kwargs)
    print("*********分割线************")

newadd = functools.partial(add,x=0)
print(newadd(7,8,c=10))
====================run_result=============
Traceback (most recent call last):
  File "2022=10/2022-10-14.py", line 100, in <module>
    print(newadd(7,8,c=10))
TypeError: add() got multiple values for argument 'x'
  • 如果functools.partial中设置的默认参数不是指定值,其会按照位置参数的顺序默认赋值,代码如下:
import functools

def add(x:int,y:int,c:int,z:int=9,*args,**kwargs):
    print(x,y,c,z)
    print(args)
    print(kwargs)
    print("*********分割线************")

newadd = functools.partial(add,100,200,300)
print(newadd(7,8))
print(newadd(1,2,3,4,5,6,7,8))

import inspect
print(inspect.signature(newadd))
==============run_result============
100 200 300 7
(8,)
{}
*********分割线************
None
100 200 300 1
(2, 3, 4, 5, 6, 7, 8)
{}
*********分割线************
None
(z: int = 9, *args, **kwargs)
lru_cache方法
  • 用法:@functools.lru_cache(maxsize=128, typed=False)带参数的装饰器函数
  • Least-recently-used装饰器,lru的含义是:最近最少使用,cache缓存
  • 如果maxsize设置为None,则禁用LRU功能,并且缓存可以无限制增长,当maxsize是二的幂时,LRU功能执行的最好
  • 如果typed设置为True,则不同类型的函数参数将单独缓存,例如:f(3)和f(3.0)则将被视为具有不同结果的不同调用
  • 返回一个字典缓存被装饰函数的调用和返回值
import time
import functools

@functools.lru_cache(typed=True)
def add(x,y,z=3):
    time.sleep(3)
    return x+y+z

print(add(4,5))
print(add(4.0,5))
print(add(y=6,x=4))
print(add(4,6))

============run_result===========
12
12.0
13
13

【代码解析】

  • 当typed为False或者不传时,print(add(4,5))print(add(4.0,5))取的是同一块缓存,从其打印时间差不多可以知道,typed为True时,这两个取的不是同一个缓存
  • 默认值传参和位置传参,两个取的缓存也不一致,及时两个传的参数相同
斐波那契数列的改造
  • 使用lru_cache装饰器改造斐波那契的递归数列,速度会很快,因为不需要每次计算
  • 代码如下:
import functools
@functools.lru_cache()
def fib(n):
    if n<2:
        return n
    return fib(n-1)+fib(n-2)

print([fib(x) for x in range(45)])

============run_result=============
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987……]
lru_cache方法的应用
  • 使用前提:
  • 同样的函数参数一定得到相同的结果
  • 函数执行时间很撑,切要多次执行
  • 本质:函数调用的参数——>返回值
  • 缺点:
  • 不支持缓存火气,key无法过期,失效
  • 不支持清除操作
  • 不支持分布式,只是一个单机的缓存(单进程,单线程)
  • 适用场景:单机上需要空间换时间的地方,可以用缓存来将计算变成快速查询;实现这种查找不会放在列表中,一般放在set或者字典中来实现
缓存的简单介绍
  • 缓存:减少后面的压力,大大提高后面的速度,让使用者觉得结果出现的特别快
  • lru_cache方法建议使用在key恒定不变的场景下,对于一段时间不变,过一段时间会变化的场景,建议不要使用此方法
  • cache(缓存)中有一个很重要的名词,叫做命中。指的是在缓存中找到某个关键词并获取的过程
  • 缓存系统在设计之后,key信息不能一直发生变化,否则会产生很多垃圾数据
  • 缓存系统在设计之后,无法实现命中功能 ,那缓存系统的存在就没有意义了