示例函式
為了開發型別檢查器,我們需要一個簡單的函式對其進行實驗。歐幾里得演算法就是一個完美的例子:
1 2 3 4 5 6 7 8 9 |
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 的一個新特性。接下來,型別檢查機制將會以一個裝飾器的形式實現,註解版本的第一行程式碼是:
1 |
def gcd(a: int, b: int) -> int: |
使用“gcd.__annotations__”可以獲得一個包含註解的字典:
1 2 3 4 |
>>> gcd.__annotations__ {'return': <class 'int'>, 'b': <class 'int'>, 'a': <class 'int'>} >>> gcd.__annotations__['a'] <class 'int'> |
需要注意的是,返回值的註解儲存在鍵“return”下。這是有可能的,因為“return”是一個關鍵字,所以不能用作一個有效的引數名。
檢查返回值型別
返回值註解儲存在字典“__annotations__”中的“return”鍵下。我們將使用這個值來檢查返回值(假設註解存在)。我們將引數傳遞給原始函式,如果存在註解,我們將通過註解中的值來驗證其型別:
1 2 3 4 5 6 7 8 |
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的返回值來測試上面的程式碼:
1 2 3 4 5 6 |
Traceback (most recent call last): File "typechecker.py", line 9, in <module> gcd(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”中獲取引數值,最後對其進行型別檢查。
得到了下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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。此時的型別檢查幾乎與第一個迴圈中相同:
1 2 3 4 |
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__)) |
得到的裝飾器程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 看起來更像原來的函式):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 型列表)