於2015年9月13日,Python 3.5釋出了,其中引人注目的一個特性,就是正式納入了PEP 0484的Type Hints提案,這個特性還可回朔相容至Python 3.2版本,實際上,官方將定型能力加入動態定型語言之中,Python並不是首例,像是PHP5中也提供了Type Hints的能力,對於習於使用動態定型語言的開發者而言,該怎麼看待這種新加入的定型能力呢?
什麼是Type Hints?
動態定型與靜態定型語言之間的爭戰已久,各有著一群死忠的擁護派,當然也有著各自堅定不移的支持理由,然而,對於其他持平看待定型能力的開發者來說,動態定型語言有其簡潔、具彈性之優點,而靜態定型語言亦有其不可抹滅的靜態檢查、工具支援等優勢,正如我在先前專欄〈從靜態定型瞭解動態定型〉中談過的,無論使用了動態定型或靜態定型,對於型態的理解與掌握,開發者本身都要負起責任。
實際上,為動態定型語言加上型態資訊的需求一直都存在著,通常也有些工具支援。以Python為例,mypy就是個第三方靜態檢查器,在最新的0.3版中,可支援在Python 2中進行型態加註,並且與PEP 0484有著更好的相容性,實際上,Python 3.5的Type Hints提案,就強烈受到了mypy的影響。可以舉個PEP 0484中Type Hints的例子來說:
def greeting(name: str) -> str: return 'Hello ' + name x = [] # type: List[Employee]
對於曾經接觸過Python的開發者來說,上述例子,第一眼可以看出的不同就是,greeting函式的簽署上,多了「: str」與「-> str」,前者標註name參數的型態必須是個str,而後者標註傳回型態要是個str,若是呼叫greeting函式時傳入了1,藉由靜態檢查工具,就可在執行前檢查出這個錯誤,而不是執行時才發現到TypeError(這也許是程式運行了一段時間之後才出錯),而這個資訊,在執行時期,也可以透過__annotations__來取得。
類似地,x變數則藉由在註解中標示了它的型態是List[Employee],因此,x參考的List,將只能存放Employee實例或其子型態。
在PEP 0484中,甚至可以看到泛型的語法,例如def first(l: Sequence[T]) -> T:的這個函式簽署,表示可以傳給first函式一個List,傳回的型態將與傳入List中之元素型態相同。
Type Hints顯然的優缺點
在PEP 0484中明白地提到,Python提供Type Hints之目的在於,提供一個型態標註的標準語法,讓Python程式碼更易於進行靜態分析、重構、進一步的執行時期型態檢查,甚至可能在某些情境下,可利用型態資訊進行效能最佳化。
如果是習慣使用靜態定型語言的開發者,而且也曾使用Python一段時間,在看到這些特性時,眼睛必然為之一亮,這不但減輕了開發時自行進行型態檢查的負擔,更重要的是,由於有了型態上的提示,讓過去Python整合開發工具上做不好的各式智慧提示、重構等功能,在實現上更容易,而且可提供更為強大的功能。
以著名的Python IDE之一PyCharm為例,在其〈Python 3.5 type hinting in PyCharm 5〉就列舉了許多讓開發上能夠更為便利、快速的功能。
當然,反對加入Type Hints的開發者,一定會舉出的缺點就是,加註型態資訊對於工具有所幫助,卻降低了程式碼的可讀性,例如PEP 0484中,就有這麼一段範例:
def async_query(on_success: Callable[[int], None], on_error: Callable[[int, Exception], None]) -> None:
初看到這個函式簽署,給我的感覺,就像是看到Java中的泛型一樣地頭痛,無論如何,這個函式簽署,絕不會比def async_query(on_success, on_error)來得簡潔、具有可讀性。
可讀性的正與反
然而,若就可讀性而言,實際上也有使用了Type Hints,卻增加了可讀性的例子。以〈Type Hints – Better type at Python〉就有個例子,在過去為了提供Sphinx產生文件,必須在註解中提供型態資訊:
def hello(name='nobody'): """Say hello to a person :param name: string value :rtype: string value """ return 'Hello' + name
有了標準的Type Hints,就可以直接藉由def hello(name: str = 'nobody') -> str這樣的函式簽署,來提供型態資訊,如果開發者在閱讀程式碼時,必須知道函式的參數與傳回值型態,後者顯然比較容易閱讀。
在不少開發者的想法中,總認為型態資訊僅是工具在進行分析時有意義,以便提供撰寫程式時更好的輔助,開發者並不需要得知型態資訊;然而,實際上有不少情況下,開發者本身必須從程式碼中獲悉型態資訊,想想看在撰寫動態定型語言時,有多少時候在為變數或參數命名時,會特意在名稱上顯露出型態資訊,像是def all(iterable):這類的函式簽署呢?
Python的Type Hints在這方面提供了選擇性,當型態資訊是有利於開發者理解程式碼時,可以直接撰寫在原始碼裏頭,而當型態資訊僅供工具分析利用的話,可以使用Stub Files,也就是將型態資訊撰寫在另一個對應名稱的.pyi檔案之中。
例如,若有個greeting.py中使用def greeting(name):定義了函式,那麼,可以在greeting.pyi中撰寫def greeting(name: str) -> str: ...定義型態資訊,同樣也可提供工具型態資訊,而開發者閱讀.py檔案內容時不受影響。
We are all consenting adults!
某些程度上,Python的Type Hints,就像只是增加了一個新的註解方式,只不過是專用於註解型態資訊,因為Python的直譯器會單純地忽略Type Hints,以x = [] # type: List[Employee]來說,在沒有其他工具輔助下,x.append(1)並不會發生錯誤,因而加上了Type Hints,並不會改變既有程式運行時的行為。因此,是否加上Type Hints,或者在哪裏加上Type Hints,開發者就有了選擇性。
從這點來看,作為一個官方標準化的「選項」,或許才是Python決定加入Type Hints的目的,畢竟需求一直都存在,有了官方標準,相關工具在類似需求時,就有了個依循,而不是各定各的規則,若真的不滿官方版本,也還有第三方的實現選項。
因此,「We are all consenting adults」這口號現在要多一份考量了,若選擇不加上Type Hints,就要自發地思考型態的問題,若選擇加上Type Hints,就得考慮該採用第三方或是官方標準,以及該要在哪些地方加上型態資訊,想清楚這是工具需要的資訊,還是開發者需要的資訊?畢竟,我們都是成熟懂事的大人了,不是嗎?