動態定型與靜態定型,如果真要選邊站,我比較想站在靜態定型語言這邊,囉嗦歸囉嗦,有時,多想一下型態的問題,總是好的。

動態定型語言界中有不少開發者,其實是知道這點的,PHP、JavaScript等也都有著靜態定型的變種語言(Hack、TypeScript等),以Python為例,自3.5、3.6都逐步在加強型態提示(Type hints),隱約能感受到吸收靜態定型優勢的野心。

Type Hints與mypy

最近在寫個Python小程式,有個函式的參數會接受字串,在打算判斷字串的起始是不是某個子字串,接著,就狐疑要呼叫的方法名稱,到底是beginswith,還是startswith了?查一下API文件是不難,然而帶著玩笑的心情,隨手在參數旁打了個:str,馬上就在編輯器的自動提示中找到startswith了,因為是簡單的小程式,所用的並不是Python IDE,而是Visual Studio Code,但沒想到真的已支援,著實小小驚奇了一下。

Python 3.5從2015年發表至今,Type Hints在工具上,似乎已有相當程度的支援了。Type Hints是規範在PEP 484,而3.6的變數標註(Variable Annotation)是規範在PEP 526,然而,如果關注Python的時間夠久,也許會想起2006年時,Python創建者Guido van Rossum就提出過PEP 3107,希望能在Python中支援函式標註(Function Annotations)。

對於在Python支援型態,其實Guido心裡更早就出現這樣的想法,據稱從2000年後就有,目前可找到的最早相關文件,是Guido在2004年發表的〈Adding Optional Static Typing to Python〉(https://goo.gl/vVQLPa),當中寫到希望可以在Python的編譯時期加入型態檢查,就文件中看到的型態語法來看,與PEP 484規範的語法有極高的相似性。

然而,無論是當時的想法以及後來的PEP 3107,都沒有獲得社群的支持,這件事就擱著,直到2013年,mypy的作者Jukka Lehtosalo在PyCon2013發表了〈Mypy: Optional Static Typing for Python〉(https://goo.gl/jLAFJp),這引起了Guido高度關注,並在2014年於python-ideas郵件清單中提議(https://goo.gl/ccvK8h)使用mypy的語法來做函式標註,之後寫了PEP 483,兩人再擴充為PEP 484。

PEP 484與526目前已獲得不少Python IDE的支援,在文字模式下,也可以透過pip安裝mypy以獲得型態檢查能力,實際上,Python標準程式庫用來支援型態標準的typing模組,也是來自於mypy。

typing模組中的有趣型態

想要瞭解Type Hints的語法,在網站上隨便搜尋看看,就可以找到不少的入門文件,然而,想要較深入認識,建議是探索一下typing模組官方說明文件。

首先,typing中定義List、Set、Iterable等通用的群集型態,例如List[int]可用來標註內含int元素的list型態,Dict[str, str]可用來標註鍵值各為str的字典,當情況變得複雜,也可以定義別名(aliases),例如:

ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]

這可以用來解決泛型結構現成巢狀後,可讀性迅速降低的問題,別名就只是型態的另一個名稱,實際上並沒有定義出新型態,例如,使用ConnectionOptions標註的參數,傳入了Dict[str, str]是可行的;如果想要定義新型態,要使用NewType,例如為int定義出UserId新型態:

UserId = NewType('UserId', int)
some_id = UserId(524313) # type: UserId

在執行時期,some_id實際上還是儲存了int,然而,在型態檢查時,不可以直接將int值指定給some_id,因為some_id的型態必須是UserId型態,這令人聯想到Haskell的Type與newtype呢!既然這樣,來看看Tuple吧!在Haskell中,Tuple實際上有著匿名型態的意義存在,在Python中使用型態標註的話,tuple就不單只是不可變的特性,組成元素的型態將決定可用於型態檢查的類型,例如x: Tuple[int, int]的話,x = (10, 'XD')就會造成編譯錯誤。

如同Guido在2014年於python-ideas郵件中,一開頭就提到的,他是在與Jukka Lehtosalo及Bob Ippolito對話時,得到了鼓舞。而Bob Ippolito先前在EuroPython 2014做了個演講〈What can python learn from Haskell?〉(https://goo.gl/edSyNd),主張將Haskell的一些特性引入Python。實際上,在typing模組中,還有Optional、Union(Haskell中Either的類似品)等有趣型態。

型態參數與泛型?

方才看到了,像List、Set、Iterable等支援泛型,自然地,開發者也會想要自訂型態參數,這可以使用typing的TypeVar,例如:

T = TypeVar('T')
def first(seq: Sequence[T]) -> T:
return seq[0]

必要的話,也可以進行型態約束,例如CT = TypeVar('CT', bound=Comparable),CT將必須是Comparable的實例。如果要定義泛型類別的話,可以使用typing模組的Generic,例如:

T = TypeVar('T') class LoggedVar(Generic[T]): 
def set(self, new: T) -> None: 
... 
def get(self) -> T:
...

Python支援物件導向,既然又面對了自定義泛型類別,那麼,不免要面對共變(Covariance),以及逆變(Contravariance)的問題。就像TypeVar在建立型態參數時,可以使用covariant=True或contravariant=True,來指定是否支援共變或逆變,例如T_co = TypeVar('T_co', covariant=True)。實際上,typing中的不可變群集,都被定義為支援共變,例如,managers: Sequence[Manager]指定給employees: Sequence[Employee]的話,是可以通過型態檢查的。

Guido的野心?

若深入看看typing模組,以及PEP 484與526,就發現它們擷取了靜態定型中的重要考量,隱隱感受到Python的野心。typing中,甚至還有個@overload裝飾器,可以定義型態檢查用的重載函式,開發者能定義同一名稱,然後簽署不同的多個函式,並提供一個執行時期實作,若呼叫了@overload定義外的函式簽署,執行型態檢查時就會發生錯誤。

當然,型態檢查是個可選的項目,就像靜態語言中,雖然可以透過型態推斷省去型態撰寫,然而在一些場合,必要時,還是會寫出型態讓程式碼變得明確。在Python中有了型態標註,也沒必要到到處標註型態,而是思考型態標註的必要性,特別是當程式碼規模變得龐大,必須更嚴謹看待型態,以及利用靜態檢查的時候。

本來Python在動態定型語言中,相對來說,就是極為重視工程性的語言,然而曾經在某個地方看過的說法是,Guido長年以來的野心,就是讓Python在工程性上更進一步,而在靜態分析工具上投入精力會是必要的。事實上,從2000年以來,Guido一直想在Python中,加入可選的靜態定型,一直到PEP 484、526的實現,本人也在PyCon 2015親自上場講演〈Type Hints 〉(https://goo.gl/wPZYWo),似乎也印證了這點。

如果開發者撰寫Python的過程中,出現了必須得思考的型態問題,現在可以認真地瞭解Type hints能否幫得上忙了,試著詳細看看typing模組的說明,瀏覽一下PEP 484、526,調查一些有哪些靜態檢查工具,應該會有意想不到的收穫。

專欄作者

熱門新聞

Advertisement