關於Python 3.5正式納入型態提示(Type hints),至今也已三年左右,實務上,可在執行程式前進行型態檢查,確實有助於察覺型態錯誤相關之臭蟲,然而對於訴求簡明的Python來說,到底何時才是使用型態提示的時機呢?

進一步瞭解細節

身為Python開發者,自3.5之後,多少都看過一些關於型態提示的相關文件吧!然而決定開始在專案中導入型態提示之前,建議還是詳細閱讀一下官方typing模組說明文件,以及規範型態提示的PEP484規範,因為,型態提示終究不是靜態定型,也不是一開始就存在Python之中,不少細節與靜態定型語言中的概念,並不相同。

正因為型態有許多的細節,對於不熟悉動態定型的Python開發者而言,建議從官方typing模組說明文件開始,以免一下子落入過多的細節。

其中,需要注意的第一件事是,對型態提示而言,函式上沒有標註型態的對象。除了實例方法與類別方法這兩個例外,都假設為Any,而不是object,前者可以支援鴨子定型,後者就真的限定為object,對於被標註為object而使用object沒定義之方法,型態檢查工具就會回報錯誤。

如果函式沒有標註傳回值型態,也是假定為Any,這表示可以傳回任何型態之實例,包括None,這是因為Python的函式若沒有return任何值,預設就會傳回None,如果函式真的不會傳回任何值,也可以標註為None,一個例子就是__init__,而且PEP484中就規範,若在__init__標註參數型態了,就一定要標註傳回值為None,絕不可省略!

如果函式可能傳回(或參數上接受)兩種型態,可以使用Union[S, T],然而,若函式可能傳回某值、也可能傳回None,建議使用Optional[T],這相當於Union[T, None],但可讀性比較好。

雖然typing模組中提供了許多可用於型態提示的名稱,不過,有些名稱並沒有定義於typing模組,然而,型態也不是模組公開之API,例如builtins模組內部的traceback類別。對於這類型態,可以使用types模組中定義之名稱,例如traceback類別就對應於TracebackType。

若你來自靜態定型語言的世界,對於型態提示也許有著更多期待,像是def foo(*args, **kwds)、泛型等的標註方式,這類細節,更多是描述於PEP484,其中還包含了cast的使用時機說明;簡單來說,越是認識細節,就越能掌握使用之時機。

何時不需要型態提示

知道應用程式不需要哪些功能,往往比知道應用程式需要哪些功能困難,類似地,知道何時不需要型態提示,也會是更重要的工作!

許多文件都說,以可讀性為最大考量,畢竟Python以簡明為哲學之一,本身也是動態定型語言,然而,若只談可讀性太過籠統,來自靜態定型語言的開發者,對型態提示帶來的可讀性,與僅接觸過動態定型語言的開發者之間,也許就有偏差。

就經驗而言,不需要型態提示的時機之一,在於函式具有過於冗長的本體實作!這時需要做的部分與型態提示無關,而是要先將函式重構為若干小函式,維持函式本體的簡短,最好是一眼就能涵蓋函式簽署,如此一來,函式本體需要標註型態的時機,就可以減少許多。

想想看,一個函式具有冗長的本體實作,本身已不易讀,再加上型態提示,視覺上更顯混亂,更何況冗長的本體實作,可能在加上型態提示之後,就令型態檢查工具丟出一堆錯誤了,這時,大概只會想放棄加註型態了吧!

絕大多數情況下,實例方法的首個參數不需要加註型態,類別方法的首參數也是,這是先前提到的,參數不會被預設為Any的兩個例外;若所在類別為C,實例方法首參數型態會預設為C,類別方法的首參數則假定為Type[C],只有很少的情況,像是繼承體系中標註必須為父類,或者是必須使用泛型時,才會標註實例方法或類別方法的首個參數。

公開的物件協定掛勾(hook)不需要標註型態,__str__、__repr__、__len__等只具有首參數為self的,自然是不用的。另外,像是__add__等運算子掛勾,通常也不需要。還有,像是情境管理器(context manager)的__enter__、__exit__等,簡單來說,它們都是明確載明於規範的協定,多半也是由執行環境呼叫,不標註型態不會有什麼問題。

函式的預設引數通常會在文件載明,函式簽署上也可以明確看到預設值,例如def foo(arg=1),從1就可以判斷arg接受int型態,這其實與先前談到函式要維持簡短的原因相同,也就是若能迅速從前後文中的值,判斷出型態,也就不用特別加上型態。

需要型態提示的時機

對於屬於函式內部流程之小函式、模組內部非公開函式等,若曾經在閱讀原始碼時,略為狐疑某參數或變數為何種型態時,就可以考慮加註型態,下次閱讀原始碼就能加快理解速度,另一方面,若曾經思考著某個型態到底實際的方法名稱為何,也可以加註型態,以便善用IDE等工具的自動提示功能。

對於內部使用的輔助函式,若真的不需要傳回值,我傾向於不加註型態,雖然此時型態提示上會預設為Any,我仍不加註None,這是因為在具有型態推斷(Type inference)的靜態定型語言中,此時,多半建議不加註傳回型態,以節省程式碼撰寫與閱讀上的麻煩。

然而,如果是個公開API,函式也確實不傳回值,建議明確標註None,以區別可以傳回任何型態的Any情況。

真正需要型態檢查的地方,當然就要標註型態,這通常發生在型態檢查工具,無法從程式碼的前後文獲取型態資訊之時。例如,def foo(arg=None),前後文若無足夠資訊,就無法知道arg是什麼型態,這時,可以考慮標註為def foo(arg: Optional[int] = None),如此一來,就知道arg是int型態,只不過預設值為None。

typing模組中有個cast函式,它並不是真的用來轉型,絕大多數情況下並不會使用,然而,若程式碼前後文資訊不足,型態檢查工具無法判定之時,可以用來提供進一步的型態資訊。

NoReturn這個標註可以留意一下,乍看它是用在函式本體沒有return任何值的情況,似乎與None重疊,不過,它的真正目的,是標註該函式必然無法正常返回,也就是連None都沒有辦法傳回的情況,函式本體必然引發例外,就是其中一種情況:

def stop() -> NoReturn:
raise RuntimeError('no way')

其應用場合之一在於,若某函式在呼叫stop()之後,仍撰寫了程式,就會是不可能執行到的(unreachable)程式碼,使用NoReturn,型態檢查工具若提供支援,就可以檢查出這類錯誤。

無關乎靜態或動態定型

動態定型語言的支持者,經常強調物件間的協定遠比型態重要,實際上,型態也是物件協定的一部份,也就是協定本就包含行為與型態,這跟語言是靜態或動態定型無關。

正如靜態定型語言,語法上過於側重型態,因而有了型態推斷,相對地,動態定型語言過於側重行為,才有了型態提示,真正重要的是,思考使用之時機,而不是淪於戰爭話題,多加瞭解,多做思考,選擇適當的使用時機,才是務實之道。

作者簡介


Advertisement

更多 iThome相關內容