Python作为一个动态的脚本语言,其函数在定义时是不需要指出参数的类型,也不需要指出函数是否有返回值。本文将介绍如何使用python的装饰器来定义一个像C++那样的强类型函数。接下去,先介绍python3中关于函数的定义。
0. python3中的函数定义
举个例子来说吧,比如如下的函数定义:
 
 def fun(a:int, b=1, *c, d, e=2, **f) -> str:
pass

这里主要是说几点与python2中不同的点。

1)分号后面表示参数的annotation,这个annotation可以是任意的python对象,不一定要type类型的,比如 a:"This is parameter a!" 这样也是可以的。
2)在可变位置参数列表c和可变关键字参数字典f中间的参数叫做 指定关键字参数(keyword only parameter),这个关键字的意思是你在函数调用时只能用关键字形式调用。比如调用下面的这个例子,调用fun(1,2)是会报trace的,因为没有指定keyword-only参数,如果调用fun(1,c=2)则不会出错。注意,如果要定义keyword-only参数,那么必须在其前面加一个可变的位置参数。
3)-> 后面的是函数的返回值,属于函数返回值的annotation。
4)虽然函数可以指定参数的annotation,但python对参数的类型还是没有限制的,比如上面的参数a不是说一定要是int类型,比如上面的函数不一定要返回一个str对象。比如下面的例子,fun的参数a可以是字符串,函数也不一定要返回值。
接下去,本文介绍利用inspect.getfullargspec()来定义一个强类型的函数。
1. inspect.getfullargspec()
 
getfullargspec函数返回一个七元组:
args:确定的参数名字列表,这个参数是定义在函数最开始的位置,这里又包括两种类型的参数,有默认参数的和没有默认参数的,并且有默认参数的必须要跟在没有默认参数的后头。
varargs:可变位置参数的名字,这种参数的位置紧接着args后面。
varkw:可变关键字参数,这种参数的位置在最后面。
defaults:确定参数的默认值元组,这个元组的第一个值是最后一个默认参数,也就是于args的排列顺序是相反的,原因嘛,仔细想想应该也就清楚了。
kwonlyargs:指定关键字参数名字,这种类型的参数其实跟args参数一样,只不过这种类型的参数必须跟在可变位置参数后面,可变关键字参数前面。另外,这种参数也分两种,有默认值和无默认值,但是与args参数不同的是有默认值和无默认值的位置没有强制的顺序,因为他们总是要指定关键字的。
kwonlydefaults:指定关键字参数的默认值字典,注意这里与defaults不同的是,defaults是元组,这里是字典,原因当然是因为指定关键字参数的默认值参数顺序不定。
annotations:这个就是参数的annotation字典,包括返回值以及每个参数分号后面的对象。
有了上面这个工具,我们就可以定义一个强类型的函数了。
2. 使用python3的函数annotation来定义强类型的函数装饰器
首先,我们必须得保证参数的annotation以及返回值必须是类型,也就是必须是Type的instance。
这里就是取出函数的annotations,然后验证是不是type的instance。现在我们可以确保所有的annotation都是有效的。接下去就是验证参数的类型是否是annotations里面所指定的type,最简单的想法当然就是看看参数名字是否在annotations中,如果在annotations中的话在看看参数值是不是annotations中指定的type。
看起来似乎非常OK,但我们来看一下这些例子:
>>> @typesafe
... def example(*args:int, **kwargs:str):
...      pass
...
>>> example(spam='eggs')
>>> example(kwargs='spam')
>>> example(args='spam')
Traceback (most recent call last):
...
TypeError: Wrong type for args: expected int, got str.
我们来分析一下出错的原因。首先这个函数的本意应该是希望所有的可变位置参数都是int型,所有的可变关键字参数都是str型。但是example函数的annotations应该是{‘args’: <class ‘int’>, ‘karges’: <class ‘str’>}字典,当取出args参数时发现这个参数名字在annotations中,就断定这个参数的值是int类型,从而导致出错。另外,如果我们调用example(spam=1)也不会出错,这于函数定义时的本意也是不符合的。综上考虑,我们还必须考虑args和kwargs参数所对应的annotation。另外,对于默认参数的类型和返回值也需要进行验证。

 # _*_ coding: utf-8
import functools
import inspect
from itertools import chain def typesafe(func):
"""
Verify that the function is called with the right arguments types and that
it returns a value of the right type, accordings to its annotations.
"""
spec = inspect.getfullargspec(func)
annotations = spec.annotations for name, annotation in annotations.items():
if not isinstance(annotation, type):
raise TypeError("The annotation for '%s' is not a type." % name) error = "Wrong type for %s: expected %s, got %s."
# Deal with default parameters
defaults = spec.defaults or ()
defaults_zip = zip(spec.args[-len(defaults):], defaults)
kwonlydefaults = spec.kwonlydefaults or {}
for name, value in chain(defaults_zip, kwonlydefaults.items()):
if name in annotations and not isinstance(value, annotations[name]):
raise TypeError(error % ('default value of %s' % name,
annotations[name].__name__,
type(value).__name__)) @functools.wraps(func)
def wrapper(*args, **kwargs):
# Populate a dictionary of explicit arguments passed positionally
explicit_args = dict(zip(spec.args, args))
keyword_args = kwargs.copy()
# Add all explicit arguments passed by keyword
for name in chain(spec.args, spec.kwonlyargs):
if name in kwargs:
explicit_args[name] = keyword_args.pop(name) # Deal with explict arguments
for name, arg in explicit_args.items():
if name in annotations and not isinstance(arg, annotations[name]):
raise TypeError(error % (name,
annotations[name].__name__,
type(arg).__name__)) # Deal with variable positional arguments
if spec.varargs and spec.varargs in annotations:
annotation = annotations[spec.varargs]
for i, arg in enumerate(args[len(spec.args):]):
if not isinstance(arg, annotation):
raise TypeError(error % ('variable argument %s' % (i+1),
annotation.__name__,
type(arg).__name__)) # Deal with variable keyword argument
if spec.varkw and spec.varkw in annotations:
annotation = annotations[spec.varkw]
for name, arg in keyword_args.items():
if not isinstance(arg, annotation):
raise TypeError(error % (name,
annotation.__name__,
type(arg).__name__)) # Deal with return value
r = func(*args, **kwargs)
if 'return' in annotations and not isinstance(r, annotations['return']):
raise TypeError(error % ('the return value',
annotations['return'].__name__,
type(r).__name__))
return r return wrapper

对于上面的代码:

19-27 行比较好理解,就是验证函数定义时,默认参数和annotation是否匹配,如果不匹配的就返回定义错误。
32 行spec.args和args的长度不一定会一样长,如果args的长度比较长,说明多出来的是属于可变位置参数,在46行到52行处理;如果spec.args的位置比较长,说明没有可变位置参数,多出来的已经通过默认值指定了,已经在19-27行处理了。
34-37 行主要是考虑到,1)有一些确定的参数在函数调用时也有可能使用参数名进行调用;2)指定关键字参数必须使用参数名进行调用。
这样处理后,keyword_args中就只存储可变关键字参数了,而explicit_args里存储的是包括确定型参数以及指定关键字参数。
39-44 行处理explicit_args里面的参数
46-53 行处理可变位置参数
55-62 行处理的是可变关键字参数
64-69 行处理返回值
 
当然这个函数对类型的要求太高了,而并没有像C++那样有默认的类型转换。以下是自己写的一个带有默认类型转换的代码,如有错误望指出。
 # _*_ coding: utf-8
import functools
import inspect
from itertools import chain def precessArg(value, annotation):
try:
return annotation(value)
except ValueError as e:
print('value:', value)
raise TypeError('Expected: %s, got: %s' % (annotation.__name__,
type(value).__name__)) def typesafe(func):
"""
Verify that the function is called with the right arguments types and that
it returns a value of the right type, accordings to its annotations.
"""
spec = inspect.getfullargspec(func)
annotations = spec.annotations for name, annotation in annotations.items():
if not isinstance(annotation, type):
raise TypeError("The annotation for '%s' is not a type." % name) error = "Wrong type for %s: expected %s, got %s."
# Deal with default parameters
defaults = spec.defaults and list(spec.defaults) or []
defaults_zip = zip(spec.args[-len(defaults):], defaults)
i = 0
for name, value in defaults_zip:
if name in annotations:
defaults[i] = precessArg(value, annotations[name])
i += 1
func.__defaults__ = tuple(defaults) kwonlydefaults = spec.kwonlydefaults or {}
for name, value in kwonlydefaults.items():
if name in annotations:
kwonlydefaults[name] = precessArg(value, annotations[name])
func.__kwdefaults__ = kwonlydefaults @functools.wraps(func)
def wrapper(*args, **kwargs):
keyword_args = kwargs.copy()
new_args = args and list(args) or []
new_kwargs = kwargs.copy()
# Deal with explicit argument passed positionally
i = 0
for name, arg in zip(spec.args, args):
if name in annotations:
new_args[i] = precessArg(arg, annotations[name])
i += 1 # Add all explicit arguments passed by keyword
for name in chain(spec.args, spec.kwonlyargs):
poped_name = None
if name in kwargs:
poped_name = keyword_args.pop(name)
if poped_name is not None and name in annotations:
new_kwargs[name] = precessArg(poped_name, annotations[name]) # Deal with variable positional arguments
if spec.varargs and spec.varargs in annotations:
annotation = annotations[spec.varargs]
for i, arg in enumerate(args[len(spec.args):]):
new_args[i] = precessArg(arg, annotation) # Deal with variable keyword argument
if spec.varkw and spec.varkw in annotations:
annotation = annotations[spec.varkw]
for name, arg in keyword_args.items():
new_kwargs[name] = precessArg(arg, annotation) # Deal with return value
r = func(*new_args, **new_kwargs)
if 'return' in annotations:
r = precessArg(r, annotations['return'])
return r return wrapper if __name__ == '__main__':
print("Begin test.")
print("Test case 1:")
try:
@typesafe
def testfun1(a:'This is a para.'):
print('called OK!')
except TypeError as e:
print("TypeError: %s" % e) print("Test case 2:")
try:
@typesafe
def testfun2(a:int,b:str = 'defaule'):
print('called OK!')
testfun2('str',1)
except TypeError as e:
print("TypeError: %s" % e) print("test case 3:")
try:
@typesafe
def testfun3(a:int, b:int = 'str'):
print('called OK')
except TypeError as e:
print('TypeError: %s' % e) print("Test case 4:")
try:
@typesafe
def testfun4(a:int = '', b:int = 1.2):
print('called OK.')
print(a, b)
testfun4()
except TypeError as e:
print('TypeError: %s' % e) @typesafe
def testfun5(a:int, b, c:int = 1, d = 2, *e:int, f:int, g, h:int = 3, i = 4, **j:int) -> str :
print('called OK.')
print(a, b, c, d, e, f, g, h, i, j)
return 'OK' print("Test case 5:")
try:
testfun5(1.2, 'whatever', f = 2.3, g = 'whatever')
except TypeError as e:
print('TypeError: %s' % e) print("Test case 6:")
try:
testfun5(1.2, 'whatever', 2.2, 3.2, 'e1', f = '', g = 'whatever')
except TypeError as e:
print('TypeError: %s' % e) print("Test case 7:")
try:
testfun5(1.2, 'whatever', 2.2, 3.2, 12, f = '', g = 'whatever')
except TypeError as e:
print('TypeError: %s' % e) print("Test case 8:")
try:
testfun5(1.2, 'whatever', 2.2, 3.2, 12, f = '', g = 'whatever', key1 = 'key1')
except TypeError as e:
print('TypeError: %s' % e) print("Test case 9:")
try:
testfun5(1.2, 'whatever', 2.2, 3.2, 12, f = '', g = 'whatever', key1 = '')
except TypeError as e:
print('TypeError: %s' % e) print('Test case 10:')
@typesafe
def testfun10(a) -> int:
print('called OK.')
return 'OK'
try:
testfun10(1)
except TypeError as e:
print('TypeError: %s' % e)
 
 
 
 

如何用python的装饰器定义一个像C++一样的强类型函数的更多相关文章

  1. $python用装饰器实现一个计时器

    直接上代码: import time from functools import wraps # 定义装饰器 def fn_timer(function): @wraps(function) def ...

  2. python基础—装饰器

    python基础-装饰器 定义:一个函数,可以接受一个函数作为参数,对该函数进行一些包装,不改变函数的本身. def foo(): return 123 a=foo(); b=foo; print(a ...

  3. 【Python】装饰器理解

    以下文章转载自:点这里 关于装饰器相关的帖子记录在这里: 廖雪峰, thy专栏, stackflow Python的函数是对象 简单的例子: def shout(word="yes" ...

  4. Python之装饰器、迭代器和生成器

    在学习python的时候,三大“名器”对没有其他语言编程经验的人来说,应该算是一个小难点,本次博客就博主自己对装饰器.迭代器和生成器理解进行解释. 为什么要使用装饰器 什么是装饰器?“装饰”从字面意思 ...

  5. 关于python的装饰器(初解)

    在python中,装饰器(decorator)是一个主要的函数,在工作中,有了装饰器简直如虎添翼,许多公司面试题也会考装饰器,而装饰器的意思又很难让人理解. python中,装饰器是一个帮函数动态增加 ...

  6. 如何写一个Python万能装饰器,既可以装饰有参数的方法,也可以装饰无参数方法,或者有无返回值都可以装饰

    Python中的装饰器,可以有参数,可以有返回值,那么如何能让这个装饰器既可以装饰没有参数没有返回值的方法,又可以装饰有返回值或者有参数的方法呢?有一种万能装饰器,代码如下: def decorate ...

  7. 第7.27节 Python案例详解: @property装饰器定义属性访问方法getter、setter、deleter

    上节详细介绍了利用@property装饰器定义属性的语法,本节通过具体案例来进一步说明. 一.    案例说明 本节的案例是定义Rectangle(长方形)类,为了说明问题,除构造函数外,其他方法都只 ...

  8. 第7.26节 Python中的@property装饰器定义属性访问方法getter、setter、deleter 详解

    第7.26节 Python中的@property装饰器定义属性访问方法getter.setter.deleter 详解 一.    引言 Python中的装饰器在前面接触过,老猿还没有深入展开介绍装饰 ...

  9. Python使用property函数和使用@property装饰器定义属性访问方法的异同点分析

    Python使用property函数和使用@property装饰器都能定义属性的get.set及delete的访问方法,他们的相同点主要如下三点: 1.定义这些方法后,代码中对相关属性的访问实际上都会 ...

随机推荐

  1. 用Spark查询HBase中的表数据

    java代码如下: package db.query; import org.apache.commons.logging.Log; import org.apache.commons.logging ...

  2. Objective-C:三种文件导入的方式比较

    三种文件导入的方式比较:   类的前项声明@class.import.include:   1.采用@class 类名的方式,它会告诉编译器有这么一个类,目前不需要知道它内部的实例变量和方法是如何定义 ...

  3. 浅析Linux线程调度

    在Linux中,线程是由进程来实现,线程就是轻量级进程( lightweight process ),因此在Linux中,线程的调度是按照进程的调度方式来进行调度的,也就是说线程是调度单元.Linux ...

  4. 动态图片 Movie android-gif-drawable GifView

    Movie 类 文档位置:/sdk/docs/reference/android/graphics/Movie.html 官方对这个类连一句介绍都没有,并且所有的方法也没有一行注释,可见多么不受重视! ...

  5. IOS Xib使用——Xib表示局部界面

    Xib文件是一个轻量级的用来描述局部界面的文件,在之前的文章讲解了为控制器添加Xib文件,本节主要讲解一下通过xib文件表示局部界面. <一> 创建Xib文件 Xib文件创建的时候是选择U ...

  6. IOS 设置圆角用户头像

    在App中有一个常见的功能,从系统相册或者打开照相机得到一张图片,然后作为用户的头像.从相册中选取的图片明明都是矩形的图片,但是展示到界面上却变成圆形图片,这个神奇的效果是如何实现的呢? 请大家跟着下 ...

  7. 解决百度ueditor支持iframe框架页面的视频播放问题

    新下载的ueditor 增加了xss 安全过虑,把iframe过滤了,导致发表的文章包含的视频播放功能被限制了. 说明:新版本ueditor要修改 xss过滤白名单 修改配置文件ueditor.con ...

  8. Android -- SlidingMenu

    实现原理 在一个Activity的布局中需要有两部分,一个是菜单(menu)的布局,一个是内容(content)的布局.两个布局横向排列,菜单布局在左,内容布局在右.初始化的时候将菜单布局向左偏移,以 ...

  9. java核心技术36讲

    https://time.geekbang.org/column/intro/82?utm_source=website&utm_medium=infoq&utm_campaign=8 ...

  10. 在Foreda8上试安装Apchehttpd-2.4.6.tar.gz

    下文是我边试边做的记录,不保证内容的完整性和正确性. 由于我的Apsire机器是最简安装Foreda8,所以需要安装httpd,熟悉一遍也是很好的嘛. 我从网上搜罗并下载了apchehttpd-2.4 ...