在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模块restful使用方法实例
Dec 10 Python
Python中的localtime()方法使用详解
May 22 Python
python把数组中的数字每行打印3个并保存在文档中的方法
Jul 17 Python
在python中实现对list求和及求积
Nov 14 Python
python下的opencv画矩形和文字注释的实现方法
Jul 09 Python
django中使用POST方法获取POST数据
Aug 20 Python
Python中的单下划线和双下划线使用场景详解
Sep 09 Python
如何在django中添加日志功能
Feb 06 Python
python列表切片和嵌套列表取值操作详解
Feb 27 Python
10个python3常用排序算法详细说明与实例(快速排序,冒泡排序,桶排序,基数排序,堆排序,希尔排序,归并排序,计数排序)
Mar 17 Python
python中shell执行知识点
May 06 Python
python3代码中实现加法重载的实例
Dec 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
PHP swfupload图片上传的实例代码
2013/09/30 PHP
php自定义函数br2nl实现将html中br换行符转换为文本输入中换行符的方法【与函数nl2br功能相反】
2017/02/17 PHP
jQuery JSON的解析方式分享
2011/04/05 Javascript
JS代码同步文本框内容的实例方法
2013/07/12 Javascript
js跳转页面方法总结
2014/01/29 Javascript
javascript中的nextSibling使用陷(da)阱(keng)
2014/05/05 Javascript
javascript记录文本框内文字个数检测文字个数变化
2014/10/14 Javascript
自定义百度分享的分享按钮
2015/03/18 Javascript
js简单实现Select互换数据的方法
2015/08/17 Javascript
js 判断各种数据类型的简单方法(推荐)
2016/08/29 Javascript
DataTables+BootStrap组合使用Ajax来获取数据并且动态加载dom的方法(排序,过滤,分页等)
2016/11/09 Javascript
javascript中json对象json数组json字符串互转及取值方法
2017/04/19 Javascript
深入研究jQuery图片懒加载 lazyload.js使用方法
2017/08/16 jQuery
JQuery判断正整数整理小结
2017/08/21 jQuery
微信小程序select下拉框实现效果
2019/05/15 Javascript
nodejs提示:cross-device link not permitted, rename错误的解决方法
2019/06/10 NodeJs
[02:51]DOTA2英雄基础教程 风暴之灵
2013/12/23 DOTA
[01:20:37]FNATIC vs NIP 2019国际邀请赛小组赛 BO2 第一场 8.16
2019/08/19 DOTA
python使用urllib模块和pyquery实现阿里巴巴排名查询
2014/01/16 Python
python根据日期返回星期几的方法
2015/07/06 Python
Python实现翻转数组功能示例
2018/01/12 Python
Python利用lxml模块爬取豆瓣读书排行榜的方法与分析
2019/04/15 Python
django搭建项目配置环境和创建表过程详解
2019/07/22 Python
python实现视频读取和转化图片
2019/12/10 Python
Selenium alert 弹窗处理的示例代码
2020/08/06 Python
size?爱尔兰官方网站:英国伦敦的球鞋精品店
2019/03/31 全球购物
销售冠军获奖感言
2014/02/03 职场文书
大型活动组织方案
2014/05/10 职场文书
环境科学专业求职信
2014/08/04 职场文书
小学庆六一主持词
2015/06/30 职场文书
个人欠条范本
2015/07/03 职场文书
小学大队长竞选稿
2015/11/20 职场文书
如何写好竞聘报告
2019/04/03 职场文书
Python Matplotlib库实现画局部图
2021/11/17 Python
阿里云服务器部署RabbitMQ集群的详细教程
2022/06/01 Servers
Python中的 No Module named ***问题及解决
2022/07/23 Python