在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实现从百度API获取天气的方法
Mar 11 Python
Python中的多行注释文档编写风格汇总
Jun 16 Python
python语言中with as的用法使用详解
Feb 23 Python
python机器学习之随机森林(七)
Mar 26 Python
Python中循环后使用list.append()数据被覆盖问题的解决
Jul 01 Python
python中redis查看剩余过期时间及用正则通配符批量删除key的方法
Jul 30 Python
python数据结构之线性表的顺序存储结构
Sep 28 Python
django创建简单的页面响应实例教程
Sep 06 Python
使用tqdm显示Python代码执行进度功能
Dec 08 Python
python 视频逐帧保存为图片的完整实例
Dec 10 Python
python 使用建议与技巧分享(四)
Aug 18 Python
python实现马丁策略回测3000只股票的实例代码
Jan 22 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
php 设计模式之 单例模式
2008/12/19 PHP
PHP中全局变量global和$GLOBALS[]的区别分析
2012/08/06 PHP
深入php define()函数以及defined()函数的用法详解
2013/06/05 PHP
php给图片加文字水印
2015/07/31 PHP
laravel实现上传图片的两种方式小结
2019/10/12 PHP
Thinkphp框架使用list_to_tree 实现无限级分类列出所有节点示例
2020/04/04 PHP
利用谷歌地图API获取点与点的距离的js代码
2012/10/11 Javascript
js实现运动logo图片效果及运动元素对象sportBox使用方法
2012/12/25 Javascript
jQuery后代选择器用法实例
2014/12/23 Javascript
AngularJS中如何使用$parse或$eval在运行时对Scope变量赋值
2016/01/25 Javascript
详解Node.js包的工程目录与NPM包管理器的使用
2016/02/16 Javascript
JS DOM实现鼠标滑动图片效果
2020/09/17 Javascript
JavaScript绑定事件监听函数的通用方法
2016/05/14 Javascript
js判断一个字符串是以某个字符串开头的简单实例
2016/12/27 Javascript
详解PHP后期静态绑定分析与应用
2018/03/21 Javascript
vue+element项目中过滤输入框特殊字符小结
2019/08/07 Javascript
[46:00]Ti4 冒泡赛第二轮LGD vs C9 2
2014/07/14 DOTA
使用IronPython把Python脚本集成到.NET程序中的教程
2015/03/31 Python
用于统计项目中代码总行数的Python脚本分享
2015/04/21 Python
pyqt5与matplotlib的完美结合实例
2019/06/21 Python
Python一行代码解决矩阵旋转的问题
2019/11/30 Python
CSS3弹性盒模型开发笔记(二)
2016/04/26 HTML / CSS
100%有机精油,美容油:House of Pure Essence
2018/10/30 全球购物
德国咖啡批发商:Coffeefair
2019/08/26 全球购物
俄罗斯建筑和装饰材料在线商店:Stroilandia
2020/07/25 全球购物
JSF如何进行表格处理及取值
2012/08/06 面试题
网游商务专员求职信
2013/10/15 职场文书
素食餐饮项目创业计划书
2014/02/02 职场文书
三月雷锋月活动总结
2014/07/03 职场文书
2014年人力资源工作总结
2014/11/19 职场文书
党校个人总结
2015/03/04 职场文书
行政经理岗位职责
2015/04/15 职场文书
休学证明范本
2015/06/19 职场文书
PhpSpreadsheet中文文档 | Spreadsheet操作教程实例
2021/04/01 PHP
vue-cli3.0修改打包后的文件名和文件地址,打包后本地运行报错解决
2022/04/06 Vue.js
SQL Server #{}可以防止SQL注入
2022/05/11 SQL Server