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. [Android UI] Activity Maintheme (Android 解决程序启动时的黑屏问题)

    <style name="MainTheme" parent="@android:style/Theme"> <item name=" ...

  2. 已有数据库(单机)部署Database Vault

    1.停止em和数据库 emctl stop dbconsole shutdown immediate 2.又一次安装DB软件 ./runInstaller 选组件的时候,选中"Oracle ...

  3. EF操作增删改查

    lianxiEntities db = new lianxiEntities();//上下文的入口 #region //EF Added //UserInfo user = new UserInfo( ...

  4. iOS开发-UITableView常用方法

    UITableView常用来展示数据,类似于Android中的ListView,相对于Android中的ListView而言,UITableView的实现是非常简单,继承UITableViewData ...

  5. 使用C#创建及调用WCF完整实例 (Windows服务宿主)

    关于WCF的概念.原理.优缺点等,在这里就不多说了,网上很多,可以自行搜索,比我解释的要专业的多. 这里直接说使用Windows 服务(Windows Service)作为宿主如何实现,其它方式不在此 ...

  6. 如何在Linux中用命令行工具管理KVM虚拟环境

    第一步: 配置存储池 Virsh命令行工具是一款管理virsh客户域的用户界面,它能在命令行中运行所给的命令以及它的参数,我们要用它给我们的KVM环境创建存储池,想知道关于这个工具的更多信息,用以下这 ...

  7. BFC是什么

    BFC 已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等).虽然我知道如何利用 BFC 解决这些问题, ...

  8. [Node.js]30. Level 6: Listen 'Question' from client, and then Answer the Question

    Clients can also answer each other questions, so let's build that feature by first listening for the ...

  9. ZOJ 3587 扩展KMP

    思路:这题确实大帝做得非常机智!字符串先求最长前缀,反的字符串再求一次最长前缀.然后就能够搞了. 每一个子串出现的次数就是最长前缀的次数嘛! #pragma comment(linker, " ...

  10. 火狐浏览器Firefox Firefox中的xpi文件是什么

    firefox中的xpi文件是什么? 问一下 最佳答案 XPI是什么? firefox中的xpi实际上类似于IE中的插件或扩展(如Flash插件等). 格式一般为.Zip, 由XUL.Javascri ...