Python 类型注解和参数类型检查

庆云11个月前技术文章724

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触发异常或者记录到日志并不触发异常(保证服务的运行)。


    相关文章

    ACOS统一监控之java应用断诊

    ACOS统一监控之java应用断诊

    一、前言对于一些使用Java语言搭建的应用架构,java的应用诊断可以帮助开发人员快速发现和解决应用程序中的问题,提高应用程序的性能和稳定性。以下是常用Java应用诊断方法:堆转储分析:使用工具如MA...

    oracle自带存储过程的压测使用

    1、使用前提条件:A、timed_statistics参数为true B、sysdba权限 C、11g及以上版本 D、ASYNCH_IO开启通过运行以下查询,确保为数据文件启用异步 I/OCOL NA...

    Ranger中Solr审计日志配置修改

    Ranger中Solr审计日志配置修改

    1、获取solr 中的rangeraudits的配置#查看其中的配置及 solrctl instancedir --list#获取配置 solrctl instancedir --get rang...

    Flink部署

    安装前准备1.1. 添加环境变量vi /etc/profile export FLINK_HOME=/opt/flinkexport PATH=$PATH:$FLINK_HOME/bin source...

    HDFS Fsimage分析磁盘目录(文件级别)

    HDFS Fsimage分析磁盘目录(文件级别)

    首先获取fsimage信息hdfs dfsadmin -fetchImage  /opt/fsimage格式化fsimage 转换为可读文本hdfs oiv -i /opt/fsimage/fsima...

     MySQL 两阶段提交

    MySQL 两阶段提交

    说明MySQL 开启 Binlog 后,所有的事务都会产生 Binlog Event,这些 Event 也是事务数据的一部分。本篇文章介绍 MySQL 如何保证事务 Binlog Event 和 In...

    发表评论    

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。