Python的靜態型別之旅

劉志軍發表於2018-10-25

本文系掘金Python月專題文章,轉載請註明來源

江湖有句話:“動態型別一時爽,程式碼重構火葬場”被廣為流傳,這話一般出自靜態語言擁護者口中,聽起來有點聳人聽聞,但也沒有想象中的那麼嚴重,Python在大型專案的應用太多了,Instagram就是最好的例子。

Python作為動態語言,在定義變數、函式返回值、方法引數都不需要指定資料型別,某種程度上讓程式碼變得無比簡潔、靈活,拋開程式執行效率,但動態語言也存在不足,例如:

1、IDE的智慧提示比較雞肋,因為無法判斷變數型別,所以IDE就不知道變數有那些屬性和方法,沒有智慧提示對老鳥來說是非常痛苦的,舉個簡單例子,印象中str有個startwith方法,但正確的寫法是 startswith,有個s,我不得不去查個文件。(不過個人建議初學者還是老老實實用編輯器手敲程式碼,以此加深記憶)

2、編譯過程中,只能發現語法錯誤,型別不匹配的問題只有等程式真正執行了才知道。雖然可以通過單元測試來規避,但是如果在程式碼編寫的過程中有IDE給我們指出來不是更好嗎。

3、介面呼叫全靠文件註釋說明,呼叫某個方法或函式,返回值和引數型別說明只能根據文件來確定。雖然我們可以要求程式設計師使用docstring或者註釋來說明函式的引數型別以及返回值型別,有個問題是即使一開始你規規矩矩的寫了docstring,但是程式碼更新之後,你的docstring可能就沒有同步更新。

這些問題在大型專案,特別是多人合作的專案上顯得尤為突出。程式碼規範、Code Review 就變得更重要了。正因為這些問題,社群對靜態型別特性的引進呼聲越來越強烈,所以在 Python3.5,也就是 PEP484 中有了型別提示(Type Hints)。定義函式時,可以指定函式的返回值型別、引數的型別。

以前寫一個函式是這樣的:

def greeting(name):
    return "Hello" + name

>>> greeting("bob")
'Hellobob'
>>> greeting(1)
TypeError: must be str, not int
複製程式碼

當你不去看文件或者原始碼的時候,你根本不知道你可以傳遞什麼型別的值進去,而當你傳入整數1時,只有等到程式執行的時候才能發現錯誤,如果有一種資料型別檢查工具在程式啟動前事先查一遍就可以避免程式出錯。

在Python3.5中,用 Type Hint 的寫法是這樣的:

def greeting(name: str) -> str:
    return 'Hello ' + name
複製程式碼

上面就是靜態型別的寫法,多了 「: str」與 「-> str」,前者用來說明 name 的型別,後者指函式返回值的型別。這樣一來,IDE像PyCharm這樣的工具也能即時的發現程式碼的問題。

Python的靜態型別之旅

當然,除了IDE之外,我們還有更強大的靜態型別檢查工具,叫 mypy,這個工具也是由Python之父GUido親自操刀實現的靜態型別檢查工具。

pip install mypy

$ mypy test.py
test.py:4: error: Argument 1 to "greeting" has incompatible type "int"; expected "str"
複製程式碼

有了型別提示,Python在程式碼呼叫、重構、甚至是靜態分析等方面有了更好的效果,不但減輕了開發時自行進行型態檢查的負擔,更重要的是,由於有了型態上的提示,讓過去Python整合開發工具上做不好的各種智慧提示、重構等功能有了統一的參考標準。

某種意義上型別提示只是一種輔助功能,雖然我們加了資料型別提示,但是對於Python直譯器來說,它會直接忽略掉型別提示資訊,即時型別有誤也不會阻止程式的執行。

而對於變數的型別,在PEP484中可以通過型別註釋來說明,就是以註釋的方式來說明變數的型別,例如:

from typing import List

x = []                # type: List[Employee]
y = [1, 2]            # type: List[int]
y.append("a")
複製程式碼

上面型別註釋表示x必須是 Employee 物件組成的列表,y必須是 int 構成的列表,整數列表y追加一個字串後,我們用 mypy 來檢查程式碼有什麼問題:

mypy test.py
xx.py:3: error: Name 'Employee' is not defined
xx.py:5: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
複製程式碼

在 Python3.6,也就是 PEP526 的提案中,針對變數註解做了進一步優化,將型別的宣告作為了語法的一部分,這樣比起註釋可讀性更強,例如:

my_var: int  # 宣告為整數型別的變數
my_var = 5  # 通過型別檢查
other_var: int = 'a'  # 給整數型別變數賦值字串,檢查器會報錯,但是直譯器執行是不會有任何問題
print(other_var)
複製程式碼
mypy xx.py  # 執行型別檢查器
xx.py:3: error: Incompatible types in assignment (expression has type "str", variable has type "int")

python test.py # 執行直譯器
a
複製程式碼

型別提示雖然給了IDE智慧提示、重構帶來了很大的便利,而恰恰因為這些型別資訊的宣告,使得動態語言變得臃腫起來,例如:

T = TypeVar('T')
S = TypeVar('S')
class Foo(Generic[T]):
    def method(self, x: T, y: S) -> S:
    # Body
複製程式碼

這是一段有泛型的註解,看起來跟Java程式碼沒什麼區別了。而諷刺的是,Java也開始加入了動態語言的特性,例如在Java10中,就有本地變數型別推斷特性,可以使用關鍵字 var 來定義變數,而不需要指定資料型別,意味著,靜態語言也開始往動態語言特性方面發展。

public class VarTest {

    public static void main(String[] args) {
        var name = "java";
        System.out.println(name);
    }
}
複製程式碼

那麼到底是靜態語言好還是動態語言好,Java和Python各自作為靜態語言和動態語言的代表,一個明顯的特點就是都在互相借鑑彼此的優點,所謂天下大勢,分久必合,合久必分。沒有一種語言是完美的,Python靈活但可控性沒那麼強,更像是一位開放的家長,在語言的處理上給開發者最大的自由。畢竟 We are all consenting adults! 而反觀Java,就像一位嚴苛的家長一樣,小心翼翼地對待每個開發者,生怕你闖禍。

相關文章