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 = '123', 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 = '123', 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 = '123', 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 = '123', 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 = '123', g = 'whatever', key1 = '111')
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)