天天看點

Python Type Hints 從入門到實踐

Python 想必大家都已經很熟悉了,甚至關于它有用或者無用的論點大家可能也已經看膩了。但是無論如何,它作為一個将加入聯考科目的語言還是有它獨到之處的,今天我們就再展開聊聊 Python。

《流暢的 Python》一書中提到,如果一門語言很少隐式轉換類型,說明它是強類型語言,例如 Java、C++ 和 Python 就是強類型語言。

同時如果一門語言經常隐式轉換類型,說明它是弱類型語言,PHP、JavaScript 和 Perl 是弱類型語言。

當然上面這種簡單的示例對比,并不能确切的說 Python 是一門強類型語言,因為 Java 同樣支援 integer 和 string 相加操作,且 Java 是強類型語言。是以《流暢的 Python》一書中還有關于靜态類型和動态類型的定義:在編譯時檢查類型的語言是靜态類型語言,在運作時檢查類型的語言是動态類型語言。靜态語言需要聲明類型(有些現代語言使用類型推導避免部分類型聲明)。

綜上所述,關于 Python 是動态強類型語言是比較顯而易見沒什麼争議的。

Python 在 PEP 484(Python Enhancement Proposals,Python 增強建議書)[https://www.python.org/dev/peps/pep-0484/]中提出了 Type Hints(類型注解)。進一步強化了 Python 是一門強類型語言的特性,它在 Python3.5 中第一次被引入。使用 Type Hints 可以讓我們編寫出帶有類型的 Python 代碼,看起來更加符合強類型語言風格。

這裡定義了兩個 greeting 函數:

普通的寫法如下:

加入了 Type Hints 的寫法如下:

以 PyCharm 為例,在編寫代碼的過程中 IDE 會根據函數的類型标注,對傳遞給函數的參數進行類型檢查。如果發現實參類型與函數的形參類型标注不符就會有如下提示:

上面通過一個 greeting 函數展示了 Type Hints 的用法,接下來我們就 Python 常見資料結構的 Type Hints 寫法進行更加深入的學習。

預設參數

Python 函數支援預設參數,以下是預設參數的 Type Hints 寫法,隻需要将類型寫到變量和預設參數之間即可。

自定義類型

對于自定義類型,Type Hints 同樣能夠很好的支援。它的寫法跟 Python 内置類型并無差別。

當類型标注為自定義類型時,IDE 也能夠對類型進行檢查。

容器類型

當我們要給内置容器類型添加類型标注時,由于類型注解運算符 [] 在 Python 中代表切片操作,是以會引發文法錯誤。是以不能直接使用内置容器類型當作注解,需要從 typing 子產品中導入對應的容器類型注解(通常為内置類型的首字母大寫形式)。

不過 PEP 585[https://www.python.org/dev/peps/pep-0585/]的出現解決了這個問題,我們可以直接使用 Python 的内置類型,而不會出現文法錯誤。

類型别名

有些複雜的嵌套類型寫起來很長,如果出現重複,就會很痛苦,代碼也會不夠整潔。

此時可以通過給類型起别名的方式來解決,類似變量命名。

這樣代碼看起來就舒服多了。

可變參數

Python 函數一個非常靈活的地方就是支援可變參數,Type Hints 同樣支援可變參數的類型标注。

IDE 仍能夠檢查出來。

泛型

使用動态語言少不了泛型的支援,Type Hints 針對泛型也提供了多種解決方案。

TypeVar

使用 TypeVar 可以接收任意類型。

Union

如果不想使用泛型,隻想使用幾種指定的類型,那麼可以使用 Union 來做。比如定義 concat 函數隻想接收 str 或 bytes 類型。

IDE 的檢查提示如下圖:

TypeVar 和 Union 差別

TypeVar 不隻可以接收泛型,它也可以像 Union 一樣使用,隻需要在執行個體化時将想要指定的類型範圍當作參數依次傳進來來即可。跟 Union 不同的是,使用 TypeVar 聲明的函數,多參數類型必須相同,而 Union 不做限制。

以下是使用 TypeVar 做限定類型時的 IDE 提示:

Optional

Type Hints 提供了 Optional 來作為 Union[X, None] 的簡寫形式,表示被标注的參數要麼為 X 類型,要麼為 None,Optional[X] 等價于 Union[X, None]。

Any

Any 是一種特殊的類型,可以代表所有類型。未指定傳回值與參數類型的函數,都隐式地預設使用 Any,是以以下兩個 greeting 函數寫法等價:

當我們既想使用 Type Hints 來實作靜态類型的寫法,也不想失去動态語言特有的靈活性時,即可使用 Any。

Any 類型值賦給更精确的類型時,不執行類型檢查,如下代碼 IDE 并不會有錯誤提示:

可調用對象(函數、類等)

Python 中的任何可調用類型都可以使用 Callable 進行标注。如下代碼标注中 Callable[[int], str],[int] 表示可調用類型的參數清單,str 表示傳回值。

自引用

當我們需要定義樹型結構時,往往需要自引用。當執行到 init 方法時 Tree 類型還沒有生成,是以不能像使用 str 這種内置類型一樣直接進行标注,需要采用字元串形式“Tree”來對未生成的對象進行引用。

IDE 同樣能夠對自引用類型進行檢查。

此形式不僅能夠用于自引用,前置引用同樣适用。

鴨子類型

Python 一個顯著的特點是其對鴨子類型的大量應用,Type Hints 提供了 Protocol 來對鴨子類型進行支援。定義類時隻需要繼承 Protocol 就可以聲明一個接口類型,當遇到接口類型的注解時,隻要接收到的對象實作了接口類型的所有方法,即可通過類型注解的檢查,IDE 便不會報錯。這裡的 Stream 無需顯式繼承 Interface 類,隻需要實作了 close 方法即可。

由于内置的 open 函數傳回的檔案對象和 Stream 對象都實作了 close 方法,是以能夠通過 Type Hints 的檢查,而字元串“s”并沒有實作 close 方法,是以 IDE 會提示類型錯誤。

實際上 Type Hints 不隻有一種寫法,Python 為了相容不同人的喜好和老代碼的遷移還實作了另外兩種寫法。

使用注釋編寫

來看一個 tornado 架構的例子(tornado/web.py)。适用于在已有的項目上做修改,代碼已經寫好了,後期需要增加類型标注。

使用單獨檔案編寫(.pyi)

可以在源代碼相同的目錄下建立一個與 .py 同名的 .pyi 檔案,IDE 同樣能夠自動做類型檢查。這麼做的優點是可以對原來的代碼不做任何改動,完全解耦。缺點是相當于要同時維護兩份代碼。

基本上,日常編碼中常用的 Type Hints 寫法都已經介紹給大家了,下面就讓我們一起來看看如何在實際編碼中中應用 Type Hints。

dataclass——資料類

dataclass 是一個裝飾器,它可以對類進行裝飾,用于給類添加魔法方法,例如 init() 和 repr() 等,它在 PEP 557[https://www.python.org/dev/peps/pep-0557/]中被定義。

以上使用 dataclass 編寫的代碼同如下代碼等價:

注意:dataclass 并不會對字段類型進行檢查。

可以發現,使用 dataclass 來編寫類可以減少很多重複的樣闆代碼,文法上也更加清晰。

Pydantic

Pydantic 是一個基于 Python Type Hints 的第三方庫,它提供了資料驗證、序列化和文檔的功能,是一個非常值得學習借鑒的庫。以下是一段使用 Pydantic 的示例代碼:

注意:Pydantic 會對字段類型進行強制檢查。

Pydantic 寫法上跟 dataclass 非常類似,但它做了更多的額外工作,還提供了如 .dict() 這樣非常友善的方法。

再來看一個 Pydantic 進行資料驗證的示例,當 User 類接收到的參數不符合預期時,會抛出 ValidationError 異常,異常對象提供了 .json() 方法友善檢視異常原因。

所有報錯資訊都儲存在一個 list 中,每個字段的報錯又儲存在嵌套的 dict 中,其中 loc 辨別了異常字段和報錯位置,msg 為報錯提示資訊,type 則為報錯類型,這樣整個報錯原因一目了然。

MySQLHandler

MySQLHandler[https://github.com/jianghushinian/python-scripts/blob/main/scripts/mysql_handler_type_hints.py]是我對 pymysql 庫的封裝,使其支援使用 with 文法調用 execute 方法,并且将查詢結果從 tuple 替換成 object,同樣也是對 Type Hints 的應用。

運作期類型檢查

Type Hints 之是以叫 Hints 而不是 Check,就是因為它隻是一個類型的提示而非真正的檢查。上面示範的 Type Hints 用法,實際上都是 IDE 在幫我們完成類型檢查的功能,但實際上,IDE 的類型檢查并不能決定代碼執行期間是否報錯,僅能在靜态期做到文法檢查提示的功能。

要想實作在代碼執行階段強制對類型進行檢查,則需要我們通過自己編寫代碼或引入第三方庫的形式(如上面介紹的 Pydantic)。下面我通過一個 type_check 函數實作了運作期動态檢查類型,來供你參考:

隻要給 greeting 函數打上 type_check 裝飾器,即可實作運作期類型檢查。

附錄

如果你想繼續深入學習使用 Python Type Hints,以下是一些我推薦的開源項目供你參考:

Pydantic [https://github.com/samuelcolvin/pydantic]

FastAPI [https://github.com/tiangolo/fastapi]

Tornado [https://github.com/tornadoweb/tornado]

Flask [https://github.com/pallets/flask]

Chia-pool [https://github.com/Chia-Network/pool-reference]

MySQLHandler [https://github.com/jianghushinian/python-scripts/blob/main/scripts/mysql_handler_type_hints.py]

TypeScript 枚舉指南

實戰經驗分享:使用 PyO3 來建構你的 Python 子產品