函数定义的弊端

  • python是动态语言,变量随时可以被赋值,且能赋值成不同的类型

  • 动态语言很灵活,但也有弊端

    1. def add(x,y):
    2. return x+y
    3. print(add(4,5))
    4. print(add('hello','world'))
    5. print(4,'hello') # 异常报错
  • 难发现,由于不做类型检查,只能在运行时才能暴露问题
  • 难使用,函数的使用者并不知道函数的设计,不知道该传什么类型的数据

解决方案

1、增加文档字符串
  • 这只是一个惯例,不是强制标准
  1. def add(x, y):
  2. '''
  3. :param x: int
  4. :param y: int
  5. :return: int
  6. '''
  7. return x+y

2、函数参数类型注解
  1. def add(x: int, y: int) -> int:
  2. """
  3. :param x:int
  4. :param y:int
  5. :return: int
  6. """
  7. return x + y
  8. print(help(add))
  9. print(add(4, 5))
  10. print(add.__annotations__)

Python3.5中引入对函数的参数进行类型注解,通过annotations可以查看

  1. {'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}

3、变量注解

变量注解在Python 3.6中引入

  1. num:int = 3

以上方案均不是强制要求。

inspect模块

inspect模块的四大功能

  • 类型检查
  • 获取源码
  • 获取类的方法和参数信息
  • 解析堆栈

signature(callable)

获取签名(函数签名包含了一个函数的信息。包括函数名、参数类型、类和名称空间的其他信息)

  1. import inspect
  2. def add(x: int, y: int, *args, **kwargs) -> int:
  3. """
  4. :param x:int
  5. :param y:int
  6. :return: int
  7. """
  8. return x + y
  9. sig = inspect.signature(add)
  10. print(sig) # 函数签名
  11. # (x:int, y:int, *args, **kwargs) -> int
  12. print('params:', sig.parameters) # 函数签名参数的有序字典
  13. # params: OrderedDict([('x', <Parameter "x:int">), ('y', <Parameter "y:int">), ('args', <Parameter "*args">), ('kwargs', <Parameter "**kwargs">)])
  14. print('return:', sig.return_annotation) # 返回值类型
  15. # return: <class 'int'>
  16. print(sig.parameters['x'].annotation) # 参数x类型
  17. # <class 'int'>
  18. for item in sig.parameters.items():
  19. name, parm = item
  20. print(parm.kind)
  • 上述示例中函数签名sig中有Parameter对象,同时包含以下属性

    • 保存在元祖中,是只读的
    • name,参数名字
    • annotation,参数的注解
    • default,参数的缺省值
    • kind,实参如何绑定到形参,就是形参的类型

      • POSITIONAL_ONLY:值必须是位置参数提供
      • POSITIONAL_OR_KEYWORD:值可以作为关键字或位置参数提供
      • VAR_POSITIONAL:可变位置参数,对应*args
      • VAR_KEYWORD:可变关键字参数,对用**kwargs
      • KEYWORDONLY:对应或_args后出现的非可变关键字参数
  • inspect.isfunction(add), 是否为函数

  • inspect.ismethod(add), 是否是类的方法

  • inspect.isgenerator(add), 是否是生成器对象

  • inspect.ismodule(inspect), 是否是模块

函数参数类型检查示例

思路:

  • 函数参数检查,一定是在函数外进行
  • 函数应该作为参数,传入到检查函数中
  • 检查函数拿到函数传入的实际参数,与形参进行对比
  • _annotations**包括返回值类型的声明,但其属性是一个无序字典,假设要做位置参数的判断,无法和**annotations_中的声明对应
  • 可使用inspect模块获取对象信息的函数签名,生成有序字典
  1. import inspect
  2. from functools import wraps
  3. def check(fn):
  4. def kwcheck(val, obj):
  5. if isinstance(val, obj): # 进行位置参数类型比较
  6. print(val, 'is', obj)
  7. else:
  8. errstr = '{} {} {}'.format(val, 'is not', obj)
  9. print(errstr)
  10. raise TypeError(errstr)
  11. @wraps(fn)
  12. def wapper(*args, **kwargs):
  13. sig = inspect.signature(fn)
  14. params = sig.parameters # 获取函数签名的有序字典
  15. # 对位置参数的处理
  16. param_list = tuple(params.keys()) # 将签名中的keys生成元组和位置参数进行比较
  17. for i, v in enumerate(args):
  18. k = param_list[i] # 通过位置参数的索引在签名中获取到对应的key
  19. kwcheck(val=v, obj=params[k].annotation)
  20. # 对关键字参数的处理
  21. for k, v in kwargs.items(): # 获取关键字参数键值对
  22. kwcheck(val=v, obj=params[k].annotation)
  23. results = fn(*args, **kwargs)
  24. return results
  25. return wapper
  26. @check
  27. def add(x: int, y: int = 7):
  28. return x + y
  29. print(add(8))