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)