程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Python >> 如何用python的裝飾器定義一個像C++一樣的強類型函數

如何用python的裝飾器定義一個像C++一樣的強類型函數

編輯:Python

    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 = '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)
 
 
 
 
     

 

       
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved