在Python 3中实现类型检查器的简单方法


Posted in Python onJuly 03, 2015

示例函数

为了开发类型检查器,我们需要一个简单的函数对其进行实验。欧几里得算法就是一个完美的例子:
 

def gcd(a, b):
  
'''Return the greatest common divisor of a and b.'''
  a = abs(a)
  b = abs(b)
  if a < b:
    a, b = b, a
  while b != 0:
    a, b = b, a % b
  return a

在上面的示例中,参数 a 和 b 以及返回值应该是 int 类型的。预期的类型将会以函数注解的形式来表达,函数注解是 Python 3 的一个新特性。接下来,类型检查机制将会以一个装饰器的形式实现,注解版本的第一行代码是:
 

def gcd(a: int, b: int) -> int:

使用“gcd.__annotations__”可以获得一个包含注解的字典:
 

>>> gcd.__annotations__
{'return': <class 'int'>, 'b': <class 'int'>, 'a': <class 'int'>}
>>> gcd.__annotations__['a']
<class 'int'>

需要注意的是,返回值的注解存储在键“return”下。这是有可能的,因为“return”是一个关键字,所以不能用作一个有效的参数名。
检查返回值类型

返回值注解存储在字典“__annotations__”中的“return”键下。我们将使用这个值来检查返回值(假设注解存在)。我们将参数传递给原始函数,如果存在注解,我们将通过注解中的值来验证其类型:
 

def typecheck(f):
  def wrapper(*args, **kwargs):
    result = f(*args, **kwargs)
    return_type = f.__annotations__.get('return', None)
    if return_type and not isinstance(result, return_type):
      raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__))
    return result
  return wrapper

我们可以用“a”替换函数gcd的返回值来测试上面的代码:

Traceback (most recent call last):
 File "typechecker.py", line 9, in <module>
  gcd(1, 2)
 File "typechecker.py", line 5, in wrapper
  raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__))
RuntimeError: gcd should return int

由上面的结果可知,确实检查了返回值的类型。
检查参数类型

函数的参数存在于关联代码对象的“co_varnames”属性中,在我们的例子中是“gcd.__code__.co_varnames”。元组包含了所有局部变量的名称,并且该元组以参数开始,参数数量存储在“co_nlocals”中。我们需要遍历包括索引在内的所有变量,并从参数“args”中获取参数值,最后对其进行类型检查。

得到了下面的代码:
 

def typecheck(f):
  def wrapper(*args, **kwargs):
    for i, arg in enumerate(args[:f.__code__.co_nlocals]):
      name = f.__code__.co_varnames[i]
      expected_type = f.__annotations__.get(name, None)
      if expected_type and not isinstance(arg, expected_type):
        raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__))
    result = f(*args, **kwargs)
    return_type = f.__annotations__.get('return', None)
    if return_type and not isinstance(result, return_type):
      raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__))
    return result
  return wrapper

在上面的循环中,i是数组args中参数的以0起始的索引,arg是包含其值的字符串。可以利用“f.__code__.co_varnames[i]”读取到参数的名称。类型检查代码与返回值类型检查完全一样(包括错误消息的异常)。

为了对关键字参数进行类型检查,我们需要遍历参数kwargs。此时的类型检查几乎与第一个循环中相同:
 

for name, arg in kwargs.items():
  expected_type = f.__annotations__.get(name, None)
  if expected_type and not isinstance(arg, expected_type):
    raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__))

得到的装饰器代码如下:
 

def typecheck(f):
  def wrapper(*args, **kwargs):
    for i, arg in enumerate(args[:f.__code__.co_nlocals]):
      name = f.__code__.co_varnames[i]
      expected_type = f.__annotations__.get(name, None)
      if expected_type and not isinstance(arg, expected_type):
        raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__))
    for name, arg in kwargs.items():
      expected_type = f.__annotations__.get(name, None)
      if expected_type and not isinstance(arg, expected_type):
        raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__))
    result = f(*args, **kwargs)
    return_type = f.__annotations__.get('return', None)
    if return_type and not isinstance(result, return_type):
      raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__))
    return result
  return wrapper

将类型检查代码写成一个函数将会使代码更加清晰。为了简化代码,我们修改错误信息,而当返回值是无效的类型时,将会使用到这些错误信息。我们也可以利用 functools 模块中的 wraps 方法,将包装函数的一些属性复制到 wrapper 中(这使得 wrapper 看起来更像原来的函数):
 

def typecheck(f):
  def do_typecheck(name, arg):
    expected_type = f.__annotations__.get(name, None)
    if expected_type and not isinstance(arg, expected_type):
      raise RuntimeError("{} should be of type {} instead of {}".format(name, expected_type.__name__, type(arg).__name__))
 
  @functools.wraps(f)
  def wrapper(*args, **kwargs):
    for i, arg in enumerate(args[:f.__code__.co_nlocals]):
      do_typecheck(f.__code__.co_varnames[i], arg)
    for name, arg in kwargs.items():
      do_typecheck(name, arg)
 
    result = f(*args, **kwargs)
 
    do_typecheck('return', result)
    return result
  return wrapper

结论

注解是 Python 3 中的一个新元素,本文例子中的使用方法很普通,你也可以想象很多特定领域的应用。虽然上面的实现代码并不能满足实际产品要求,但它的目的本来就是用作概念验证。可以对其进行以下改善:

  •     处理额外的参数( args 中意想不到的项目)
  •     默认值类型检查
  •     支持多个类型
  •     支持模板类型(例如,int 型列表)
Python 相关文章推荐
Python实现比较两个列表(list)范围
Jun 12 Python
快速排序的算法思想及Python版快速排序的实现示例
Jul 02 Python
Python常见异常分类与处理方法
Jun 04 Python
Django中Forms的使用代码解析
Feb 10 Python
python学生管理系统学习笔记
Mar 19 Python
Python批量生成幻影坦克图片实例代码
Jun 04 Python
python list多级排序知识点总结
Oct 23 Python
Django框架安装及项目创建过程解析
Sep 14 Python
在python中对于bool布尔值的取反操作
Dec 11 Python
Python中全局变量和局部变量的理解与区别
Feb 07 Python
在python中实现导入一个需要传参的模块
May 12 Python
详解Python生成器和基于生成器的协程
Jun 03 Python
python插入排序算法实例分析
Jul 03 #Python
python列出目录下指定文件与子目录的方法
Jul 03 #Python
python清除字符串里非字母字符的方法
Jul 02 #Python
python清除字符串里非数字字符的方法
Jul 02 #Python
python实现在控制台输入密码不显示的方法
Jul 02 #Python
python获取外网ip地址的方法总结
Jul 02 #Python
python实现将英文单词表示的数字转换成阿拉伯数字的方法
Jul 02 #Python
You might like
关于laravel 日志写入失败问题汇总
2019/10/17 PHP
JavaScript的Cookies
2008/01/16 Javascript
JavaScript 判断浏览器类型及版本
2009/02/21 Javascript
javascript 时间比较实现代码
2009/10/28 Javascript
javascript Onunload与Onbeforeunload使用小结
2009/12/31 Javascript
js 回车提交表单两种实现方法
2012/12/31 Javascript
JS文本框默认值处理详解
2013/07/10 Javascript
js控制href内容的连接内容的变化示例
2014/04/30 Javascript
Javascript中的getUTCDay()方法使用详解
2015/06/10 Javascript
javascript实现根据iphone屏幕方向调用不同样式表的方法
2015/07/13 Javascript
使用requestAnimationFrame实现js动画性能好
2015/08/06 Javascript
在javascript中创建对象的各种模式解析
2016/05/16 Javascript
JS 组件系列之Bootstrap Table的冻结列功能彻底解决高度问题
2017/06/30 Javascript
jQuery图片加载失败替换默认图片方法汇总
2017/11/29 jQuery
通过vue-router懒加载解决首次加载时资源过多导致的速度缓慢问题
2018/04/08 Javascript
vue 设置proxyTable参数进行代理跨域
2018/04/09 Javascript
vue3.0 CLI - 3.2 路由的初级使用教程
2018/09/20 Javascript
vue+vant 上传图片需要注意的地方
2021/01/03 Vue.js
python模仿网页版微信发送消息功能
2018/02/24 Python
Python对excel文档的操作方法详解
2018/12/10 Python
python之线程通过信号pyqtSignal刷新ui的方法
2019/01/11 Python
python 模拟创建seafile 目录操作示例
2019/09/26 Python
Django多进程滚动日志问题解决方案
2019/12/17 Python
基于nexus3配置Python仓库过程详解
2020/06/15 Python
Python如何实现后端自定义认证并实现多条件登陆
2020/06/22 Python
Python selenium键盘鼠标事件实现过程详解
2020/07/28 Python
若干个Java基础面试题
2015/05/19 面试题
高级电工工作职责
2013/11/21 职场文书
项目合作计划书
2014/01/09 职场文书
护士检查书
2014/01/17 职场文书
学院党的群众路线教育实践活动整改方案
2014/10/04 职场文书
领导干部整治奢华浪费之风思想汇报
2014/10/07 职场文书
超市收银员岗位职责
2015/04/07 职场文书
如何写好竞聘报告
2019/04/03 职场文书
redis客户端实现高可用读写分离的方式详解
2021/07/04 Redis
JS封装cavans多种滤镜组件
2022/02/15 Javascript