示例函數
為了開發類型檢查器,我們需要一個簡單的函數對其進行實驗。歐幾里得算法就是一個完美的例子:
?
def gcd(a, b): '''Return the greatest common divisor of a and b.''' a = abs(a) b = abs(b) if a < b: a, b = b, a while b != 0: a, b = b, a % b return a
在上面的示例中,參數 a 和 b 以及返回值應該是 int 類型的。預期的類型將會以函數注解的形式來表達,函數注解是 Python 3 的一個新特性。接下來,類型檢查機制將會以一個裝飾器的形式實現,注解版本的第一行代碼是:
?
def gcd(a: int, b: int) -> int:
使用“gcd.__annotations__”可以獲得一個包含注解的字典:
?
>>> gcd.__annotations__ {'return':, 'b': , 'a': } >>> gcd.__annotations__['a']
需要注意的是,返回值的注解存儲在鍵“return”下。這是有可能的,因為“return”是一個關鍵字,所以不能用作一個有效的參數名。
檢查返回值類型
返回值注解存儲在字典“__annotations__”中的“return”鍵下。我們將使用這個值來檢查返回值(假設注解存在)。我們將參數傳遞給原始函數,如果存在注解,我們將通過注解中的值來驗證其類型:
?
def typecheck(f): def wrapper(*args, **kwargs): result = f(*args, **kwargs) return_type = f.__annotations__.get('return', None) if return_type and not isinstance(result, return_type): raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__)) return result return wrapper
我們可以用“a”替換函數gcd的返回值來測試上面的代碼:
Traceback (most recent call last): File "typechecker.py", line 9, ingcd(1, 2) File "typechecker.py", line 5, in wrapper raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__)) RuntimeError: gcd should return int
由上面的結果可知,確實檢查了返回值的類型。
檢查參數類型
函數的參數存在于關聯代碼對象的“co_varnames”屬性中,在我們的例子中是“gcd.__code__.co_varnames”。元組包含了所有局部變量的名稱,并且該元組以參數開始,參數數量存儲在“co_nlocals”中。我們需要遍歷包括索引在內的所有變量,并從參數“args”中獲取參數值,最后對其進行類型檢查。
得到了下面的代碼:
?
def typecheck(f): def wrapper(*args, **kwargs): for i, arg in enumerate(args[:f.__code__.co_nlocals]): name = f.__code__.co_varnames[i] expected_type = f.__annotations__.get(name, None) if expected_type and not isinstance(arg, expected_type): raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__)) result = f(*args, **kwargs) return_type = f.__annotations__.get('return', None) if return_type and not isinstance(result, return_type): raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__)) return result return wrapper
在上面的循環中,i是數組args中參數的以0起始的索引,arg是包含其值的字符串。可以利用“f.__code__.co_varnames[i]”讀取到參數的名稱。類型檢查代碼與返回值類型檢查完全一樣(包括錯誤消息的異常)。
為了對關鍵字參數進行類型檢查,我們需要遍歷參數kwargs。此時的類型檢查幾乎與第一個循環中相同:
?
for name, arg in kwargs.items(): expected_type = f.__annotations__.get(name, None) if expected_type and not isinstance(arg, expected_type): raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__))
得到的裝飾器代碼如下:
?
def typecheck(f): def wrapper(*args, **kwargs): for i, arg in enumerate(args[:f.__code__.co_nlocals]): name = f.__code__.co_varnames[i] expected_type = f.__annotations__.get(name, None) if expected_type and not isinstance(arg, expected_type): raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__)) for name, arg in kwargs.items(): expected_type = f.__annotations__.get(name, None) if expected_type and not isinstance(arg, expected_type): raise RuntimeError("{} should be of type {}; {} specified".format(name, expected_type.__name__, type(arg).__name__)) result = f(*args, **kwargs) return_type = f.__annotations__.get('return', None) if return_type and not isinstance(result, return_type): raise RuntimeError("{} should return {}".format(f.__name__, return_type.__name__)) return result return wrapper
將類型檢查代碼寫成一個函數將會使代碼更加清晰。為了簡化代碼,我們修改錯誤信息,而當返回值是無效的類型時,將會使用到這些錯誤信息。我們也可以利用 functools 模塊中的 wraps 方法,將包裝函數的一些屬性復制到 wrapper 中(這使得 wrapper 看起來更像原來的函數):
?
def typecheck(f): def do_typecheck(name, arg): expected_type = f.__annotations__.get(name, None) if expected_type and not isinstance(arg, expected_type): raise RuntimeError("{} should be of type {} instead of {}".format(name, expected_type.__name__, type(arg).__name__)) @functools.wraps(f) def wrapper(*args, **kwargs): for i, arg in enumerate(args[:f.__code__.co_nlocals]): do_typecheck(f.__code__.co_varnames[i], arg) for name, arg in kwargs.items(): do_typecheck(name, arg) result = f(*args, **kwargs) do_typecheck('return', result) return result return wrapper
結論
注解是 Python 3 中的一個新元素,本文例子中的使用方法很普通,你也可以想象很多特定領域的應用。雖然上面的實現代碼并不能滿足實際產品要求,但它的目的本來就是用作概念驗證。可以對其進行以下改善:
- ??? 處理額外的參數( args 中意想不到的項目)
- ??? 默認值類型檢查
- ??? 支持多個類型
- ??? 支持模板類型(例如,int 型列表)
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
