在Python相關的文件、書籍,越來越常看到型態提示(Type Hints)的運用,Google、Microsoft、Facebook等大廠也各自貢獻了型態檢查工具,新的PEP也不斷地在新版本的Python中實現。

Python 3被忽略的特色?

如果稍微時光倒流一下,回到2008年Python 3.0剛釋出的那個時空,談到它最引人注目的特性,八成都會提及對Unicode的支援、print不再是陳述句而是個函式、許多傳回iterable物件的函式等,絕不會提及為型態提示做的準備(PEP3107)──畢竟動態定型語言才是Python的賣點,當時對型態提示的討論也不多。

然而,從Python 3.5實現PEP484,正式支援型態提示以後,每個新版本都進一步增強型態提示的功能,像是Python 3.6實現了PEP526,3.7實現PEP561、563,3.8的544、PEP586、589、591,以及3.9實現的PEP585,從這個發展歷程來看,型態提示完全就是Python 3.5以後各版本的特性重點。

從PEP的歷史來看,Python 3.0早就為型態提示做好準備,我在先前專欄〈Type Hints的野心〉就提過,3.0就實現了PEP3107,雖然當時沒有獲得社群的關注,然而,函式標註(Function Annotations)正是型態提示的基礎,Python 3.5的函式型態標註PEP484,就是基於PEP3107櫎充而來。

當然,在對的時空下提出的功能特性,才會有舞臺。現今型態提示在開發者間的接受度,可以從檢查工具的支援程度上看出端倪。除了IDE提供的支援、開發者熟悉的mypy之外,Google提供的pytype、Facebook貢獻的Pyre、Microsoft釋出的pyright,現今都是可用的型態檢查工具之一。

撰寫這篇文件的時候,Python即將迎來3.9,當中實現的PEP585,讓內建的群集(collections)型態,可以直接支援泛型,不用再依賴typing模組的對應型態,例如,可以直接標註list[int],不必再使用typing.List[int],collections.abc模組中的型態,也都支援泛型了。

回顧3.5到3.9版,我們可以看出,Python 3有一半的歷史,都在增強型態提示,現今若再提及Python 3的特色,型態提示已經是個絕對要提及的特色了。

PEP586 – Literal Types

如果開發者關注型態提示,或者是具有使用靜態定型語言的使用經驗,我們可以發現,Python 3.7已經實現了大多數型態檢查需要的功能,也就是先前專欄〈Type Hints的野心〉提過的一些特性,那麼,後續版本的新特色有什麼呢?

Python 3.8值得注意的是Literal Types,只不過,就算是有著靜態定型語言經驗的開發者,對於這個特性,可能也會感到陌生。

如果去搜尋一下Literal Types,我們會發現多半是在談TypeScript的Literal Types,這其實沒什麼好訝異的,因為,Python之父Van Rossum就曾公開表示,Python型態提示的設計靈感,就是來自於TypeScript,而Literal Types顯然也是其中之一。

Literal Types的作用,在於透過typing模組的Literal,可以為特定的一組「值」定義型態,例如,Gene = Literal['A', 'C', 'G', 'T']定義了Gene型態,在這個型態下,只有四個值'A'、'C'、'G'、'T',如果撰寫g: Gene = 'A'(或另三個值)可以通過型態檢查,然而g: Gene = 'B'會失敗,這是因為'B'不在Gene的型態定義。

這特色乍看令人不明就裡,其中,常見的疑問之一是:為什麼會有特定一組值構成的型態?

其實,Python的bool型態就是實際案例了:bool型態只有True、False兩個值。另一個常見的疑問是,這跟列舉(Enum)的差別是?定義Gene = Enum('Gene', ('A', 'C', 'G', 'T')),撰寫g: Gene = Gene.A,不也能有型態檢查的作用嗎?

單就型態檢查而言,兩者確實在功能上有重疊,其實重點在於比較g: Gene = 'A'與g: Gene = Gene.A後續程式碼的差別。讓我們來設想一個情境:開發者已經基於'A'、'C'、'G'、'T'實作了函式,後來發現有其他開發者可能會誤傳其他值作為引數,因而想透過型態提示結合工具,來檢查出這個問題,哪個做法會比較好呢?

透過Literal Types,只要將參數改為g: Gene,函式本體都不用改;透過列舉的話,函式實作就需要更動,例如,修改為g.name來取得列舉名稱之類的。簡單來說,Literal Types可以讓其他程式碼不用為了增加型態提示而變動,PEP586中以標準程式庫open函式描述應用場景,也是同樣的道理。

PEP544 – Protocols

Python是動態定型語言,在多型的表現上,採用鴨子定型,也就是物件只要具有對應的方法,無論型態為何,就可以操作,例如Python當中,只要具有__iter__方法的就是iterable物件,就可以參與for in語法,而且,目前Python也大量依賴這類的協定在運作。

然而,物件如果不具有正確的協定,錯誤只會在執行時期呈現,繼承abc模組的ABC類別,同樣也是執行時期才能發覺問題,這時,就有了個需求,可不可以是鴨子定型,又可以進行靜態檢查呢?

在靜態定型語言中的需求是反過來的,想要能進行靜態檢查,又能具有鴨子定型的彈性,因而有了結構化定型(Structural typing)的特性,例如Go、Scala都具有這個特性,其實Python 3.8之前,也有部份這個特性,例如透過typing.Iterable,撰寫iterable: Iterable = [1, 2, 3]的話可以通過型態檢查,然而iterable: Iterable = 1就不行。

問題在於,對於typing沒有提供的協定型態,Python 3.7以前沒辦法自行定義,那麼,在3.7以前,是如何達到Iterable這類協定的型態檢查呢?其實是有個內部的_Protocol用來實現Iterable等協定型態,若真的想在3.7前獲得這個特性,我們可以透過安裝typing_extensions來解決。

Python 3.8實現了PEP544,正式提供了typing.Protocol,它的metaclass指定了內部的_ProtocolMeta,_ProtocolMeta是abc.ABCMeta子類別,想要定義協定,可以繼承Protocol,例如:

class Duck(Protocol):
def quack(self):
passe

這麼一來,Duck就可以作為型態提示,只有具備quack方法的物件(然而不必繼承Duck),才可以通過型態檢查,由於Protocol的metaclass指定的是abc.ABCMeta的子類別,實際上,它也是個抽象類別,若有類別繼承了Duck,執行時期也會檢查是否實現了quack方法。

PEP591 - Final

Python 3.8實現的PEP591,也是個值得注意的特性,因為可透過typing.Final來實現「常數」,例如PI: Final = 3.14159,這麼一來,若後續有程式碼試圖重新指定PI的值,就會被型態檢查工具檢驗出來,而且,Final也支援泛型,因此可以進一步地撰寫為PI: Final[float] = 3.14159。

當然,每個規範型態提示的PEP,都包含了許多語法細節,以及為何會有該份PEP的歷史說明,建議你查看一下上述提及的PEP,來獲得更多型態提示方面的細節,而在〈Python static type checking〉當中,也提供了依Python版本列出的型態提示PEP清單,是個不錯的參考。

總而言之,就現況來說,型態提示佔了Python 3重大特性當中極重的份量,這也意味著開發者會越來越常遇上它,所以,如果使用Python 3,就別再忽略型態提示了,而且,如果這麼做,基本上,等同於忽略了Python 3一半的功能!

作者簡介


Advertisement

更多 iThome相關內容