在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实现TCP/IP协议下的端口转发及重定向示例
Jun 14 Python
基于Python 装饰器装饰类中的方法实例
Apr 21 Python
Python使用到第三方库PyMuPDF图片与pdf相互转换
May 03 Python
Python使用正则表达式分割字符串的实现方法
Jul 16 Python
python函数的作用域及关键字详解
Aug 20 Python
通过python扫描二维码/条形码并打印数据
Nov 14 Python
Python实现将元组中的元素作为参数传入函数的操作
Jun 05 Python
Python smtp邮件发送模块用法教程
Jun 15 Python
python3获取控制台输入的数据的具体实例
Aug 16 Python
详解Python牛顿插值法
May 11 Python
Python如何配置环境变量详解
May 18 Python
使用Python脚本对GiteePages进行一键部署的使用说明
May 27 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
基于mysql的论坛(6)
2006/10/09 PHP
php模拟用户自动在qq空间发表文章的方法
2015/01/07 PHP
PHP编写文件多服务器同步程序
2016/07/02 PHP
让textarea控件的滚动条怎是位与最下方
2007/04/20 Javascript
img的onload的另类用法
2008/01/10 Javascript
Extjs学习笔记之七 布局
2010/01/08 Javascript
jsp js鼠标移动到指定区域显示选项卡离开时隐藏示例
2013/06/14 Javascript
jQuery实现右侧显示可向左滑动展示的深色QQ客服效果代码
2015/10/23 Javascript
jquery对dom节点的操作【推荐】
2016/04/15 Javascript
js获取当前时间(昨天、今天、明天)
2016/11/23 Javascript
使用Angular缓存父页面数据的方法
2017/01/03 Javascript
jQuery中的$是什么意思及 $. 和 $().的区别
2018/04/20 jQuery
js遍历添加栏目类添加css 再点击其它删除css【推荐】
2018/06/12 Javascript
vue-awesome-swiper 基于vue实现h5滑动翻页效果【推荐】
2018/11/08 Javascript
JavaScript函数式编程(Functional Programming)高阶函数(Higher order functions)用法分析
2019/05/22 Javascript
js判断在哪个浏览器打开项目的方法
2020/01/21 Javascript
vue3+typeScript穿梭框的实现示例
2020/12/29 Vue.js
分析并输出Python代码依赖的库的实现代码
2015/08/09 Python
Python使用PyCrypto实现AES加密功能示例
2017/05/22 Python
python批量替换多文件字符串问题详解
2018/04/22 Python
python清空命令行方式
2020/01/13 Python
解决jupyter notebook import error但是命令提示符import正常的问题
2020/04/15 Python
Pycharm修改python路径过程图解
2020/05/22 Python
伦敦高级内衣品牌:Agent Provocateur(大内密探)
2016/08/23 全球购物
阿联酋团购网站:Groupon阿联酋
2016/10/14 全球购物
瑞典快乐袜子:Happy Socks
2018/02/16 全球购物
Maison Lab荷兰:名牌Outlet购物
2018/08/10 全球购物
Foot Locker德国官方网站:美国运动服和鞋类零售商
2018/11/01 全球购物
单位创先争优活动方案
2014/01/26 职场文书
交通运输局四风问题对照检查材料思想汇报
2014/10/09 职场文书
2014年专项整治工作总结
2014/11/17 职场文书
社区党员干部承诺书
2015/05/04 职场文书
法人代表资格证明书
2015/06/18 职场文书
深度学习tensorflow基础mnist
2021/04/14 Python
关于Python OS模块常用文件/目录函数详解
2021/07/01 Python
MySQL数据库 任意ip连接方法
2022/05/20 MySQL