基本上,Python是一門動態定型語言,然而,從Python 3.5開始,加入了型態提示(Type Hints),到了3.6,則將暫定(provisional)狀態的typing模組,正式納入標準API,並增加註解區域變數型態的特性。
在Python中能宣告型態,已不是新聞,既然如此,何妨深入一下呢?
直譯器忽略型態提示?
在先前專欄〈思考Python的型態提示〉以及〈Type Hints的野心〉,曾經談過Python中為何加入型態提示,以及相對於動態定型語言來說,型態提示還具有什麼樣的可能性。
相信許多開發者,也已經或多或少地,在專案中使用型態提示的功能了,那麼,可曾想過,為什麼使用型態提示時,通常還要搭配typing模組,而且,它還是在Python 3.6之後,才正式成為標準API?
這表示它就是個模組程式庫,而不是語法嗎?
為什麼型態標註不能直接使用內建型態?
當然,在簡單的情況下,使用內建的型態,其實就足夠了。
以Python 3.6為例,如果想限定區域變數只能指定字串,我們可以撰寫name: str = 'Justin',開發者能使用int、str、list、set、dict等內建型態來標註變數型態,實際上,在自定義的類別當中,像是若有個User類別,也可以user: User = User('Justin')來進行這樣的標註。
問題在於,想要進一步地,限定list中只能有int呢?查看型態提示相關的說明或教學文件,基本上,都說要from typing import List,然後使用names: List[str]的方式進行標註。但如果沒有typing模組,是否還能進行標註呢?names: list[str]是行不通的,因為使用python直譯器執行時,就會出現'type' object is not subscriptable了。
嗯?python直譯器不是會忽略型態提示嗎?這並不是完全正確的答案,因為,冒號左邊的標註是會被執行的!
實際上,標註型態時,冒號右邊必須是個type實例,或者說必須傳回type實例,該實例提供了必要的型態資訊,如果願意,對於下列這類標註型態變數,python直譯器也是會忽略型態標註:
def Str():
return str
x: Str() = 'Justin'
深入typing模組
不過,Str()並不是型態提示規範中預期的協定,因此僅僅是python直譯器忽略罷了,若是使用mypy進行型態檢查,則會出現invalid type comment or annotation的錯誤,因為,mypy預期必須是[]的標註方式。
熟悉Python的開發者,此時,應該馬上就會想到[]與__getitem__這特殊的方法掛鉤之間的關係,可以用來標註型態的物件,必然實作了__getitem__,而some[xxx]的結果,必然是傳回類型實例,一個單純的範例就是:
class Foo:
def __getitem__(self, type):
return type
foo = Foo()
x: foo[str] = 'Justin'
這會使得mypy的錯誤訊息轉而變成Invalid type,當然!因為被傳回的類型實例,必須提供相關的型態資訊,以供mypy之類的工具提取資訊進行型態檢查,因而還有其他相關協定必須實作,不過,有了這樣的出發點,接著,就可以開始探索typing模組的原始碼了。
在typing模組的原始碼中,有個值得特別注意的get_type_hints函式,其docstrings中寫到,基本上與obj.__annotations__作用相同,__annotations__是用來獲得標註資訊的,例如,若在模組層級撰寫了name: str = 'Justin',那麼,透過模組實例的__annotations__,例如globals()['__annotations__'],就可以取得{'name': <class 'str'>}資訊。
透過typing模組的get_type_hints函式,還可以指定作用範圍,因而能更方便地指定模組、類別、函式等實例,從而取得被標註的類型。
例如,若有個函式def add(n1: int, n2: int) -> int,透過typing.get_type_hints(add),可以取得# -> {'n1': <class 'int'>, 'n2': <class 'int'>, 'return': <class 'int'>}這樣的資訊。
執行時期型態驗證
等等!無論是透過__annotations__,或者是透過get_type_hints函式,這些不都是執行時期出現的行為嗎?
沒錯!這也就表示,型態標註資訊本身,不僅僅只能用於靜態時期分析,也可以用於執行時期檢查,例如,可以實作一個check_args函式:
def check_args(obj, **kwargs):
for name, type in get_type_hints(obj).items():
if name != 'return' and not isinstance(kwargs[name], type):
raise TypeError(f'{name} is not of type {type}')
def add(n1: int, n2: int) -> int:
check_args(add, n1 = n1, n2 = n2)
return n1 + n2
這麼一來,add(1, 2)可以順利執行,然而像是add(1, '2'),就會出現TypeError: n2 is not of type <class 'int'>的錯誤訊息。
當然,如果覺得在函式中,還要特地呼叫check_args的方式很不自然,也許就設計個@type_check裝飾器(可參考https://goo.gl/94Zwjv),如此一來,就可以使用標註來決定,是否在執行時期進行型態檢查:
@type_check
def add(n1: int, n2: int) -> int:
return n1 + n2
型態提示的趣味用法
簡單來說,Python的型態提示,並不只是用在靜態時期分析,在執行時期也可以獲取被標註的型態資訊,從而可以透過型態提示來進行執行時期型態檢查。
既然如此,過去為了模擬靜態定型語言的一些特性,而使用了動態時期型態檢查的作法,就有可能透過型態提示來完成,並且令撰寫上更自然,甚至接近靜態定型語言的寫法。
例如,動態定型語言基本上無法重載,然而有個overloading.py(https://goo.gl/wn45dp)專案,可以搭配型態提示語法,在語法外觀與實質效用上,達到重載的效果;而在Python 3.6標準typing模組中,也有個overload可以使用。
另外,還有pydantic(https://goo.gl/Hyhjcp),可以搭配型態提示來對外部資料進行驗證。
當然,上述這些都是執行時期的行為。
在剛開始運用型態提示時,from typing import xxx這動作看似平常,然而,在懷疑為何這動作是必要之後,其實可以引發一連串有趣的探討,有機會的話,挖挖看typing模組的原始碼,以及其他有創意的開放原始碼程式庫,或許可以發現,甚至發明更多型態提示趣味且實用的成品。