Python 的型別提示 Type Hint
Python 如何把關資料的出入?
文章目錄
Type Hint 是什麼?
型別提示(Type Hint)是 Python 從 3.5 版本開始所引入的一項功能,它讓開發者可以在程式碼中「標註」變數、函式參數以及回傳值的資料型別。
如果有使用過靜態語言的開發者,或是像筆者一樣有在寫 TypeScript,那對這種語法應該不陌生。
Python 是動態型別的程式語言,動態的意思是,程式碼中的變數型別可能會一直變動,開發者不需要在宣告變數時指定資料型別,並且在定義完之後也能夠更改變數的型別。
這樣看起來彈性,但常會導致以下問題:
-
程式碼可讀性降低: 在沒有型別提示的情況下,跟你合作的開發者(或未來的你自己)可能難以理解函式會接收什麼樣的參數型別,或者會傳回什麼型別的值。
-
除錯困難: 型別不匹配的錯誤只會在執行時會發生,而且不容易除錯。
雖然這麼說,但其實型別提示並沒有強制性,即便你在 Python 的程式碼中標上型別提示,也不會執行靜態分析(運行程式碼之前,自動偵測出型別不匹配之處)。
所以僅有型別提示,嚴格來說只會帶來可讀性以及易於除錯,但除此之外,還是有一些優點,像是 IDE 的自動補齊以及錯誤提示功能上,如果你有確實加上型別提示,這些提示功能會更精準。
而且當你打算引入靜態型別檢查工具(例如 mypy、pyright)時,Python 就可以變得像 TypeScript 一樣,在程式碼執行前執行靜態分析,在開發的早期就發現潛在的型別問題。
之後會提到的 Pydantic 也是以型別提示為基礎來進行資料驗證。
型別提示的使用方式如下(範例取自 FastAPI 官網):
def get_full_name(first_name: str, last_name: str):
full_name = first_name.title() + " " + last_name.title()
return full_name
print(get_full_name("john", "doe"))
以上程式碼是在函式的引數中,定義引數的型別。這樣看來感覺型別提示有點囉唆,似乎放棄了 Python 動態語言的靈活與彈性,但這種取捨,會帶來了靜態語言的嚴謹性,讓你的程式碼的可維護性大幅提升,除錯不會除到眼花(雖然這年頭人工除錯的機會越來越少了)。
帶有類型參數的泛型類型
有些資料結構可以包含其他類型的資料,例如 Python 當中的 List, Tuple, Set, Dict 這類。
其中,List, Tuple, Set 的情況相當單純:
def process_items(items: list[str]):
for item in items:
print(item)
def process_items(items: tuple[int, int, str]):
for item in items:
print(item)
def process_items(items: set[bytes]):
for item in items:
print(item)
要特別注意的是,由於 Tuple 的特性:變數宣告後,Tuple 的內部元素不管是數量、順序或是個別元素本身都不能再更動,所以 Tuple 內部元素的資料類型就必須一次宣告完畢。
這是與內部元素會變動的 List 與 Set 不太一樣的地方。
聯合型別 Union
多種資料輸入
有些變數可能會需要多個種類的資料輸入,這時候就可以用上聯合型別:
def process_item(item: int | str):
print(item)
上面的意思是:process_item 這個函式會輸入整數或是字串型別的參數,任一皆可。
可選參數
有些參數的狀況比較特殊,函數的參數有可能會有,也有可能沒有,這時候也可以使用 Union 來指定這種狀況:
def say_hi(name: str | None = None):
if name is not None:
print(f"Hey {name}!")
else:
print("Hello World")
上面的狀況表示,say_hi 函式所接受的參數,有可能是字串或是沒有指定,如果沒有指定變數,函式就依照判斷,改為執行沒有參數輸入的程式碼。
Class 型別
你可以定義一個 class 當成變數的型別,就像這樣:
class Person:
def __init__(self, name: str):
self.name = name
def get_person_name(one_person: Person):
return one_person.name
這樣我們可以清楚知道,函式輸入的參數有定義好一個 name 屬性。在物件導向中,參數為 Person類別的實例化。
加上 Pydantic 升級成資料驗證
在先前介紹 FastAPI 一文中有提到 Pydantic,它不但是 FastAPI 的標配之一,也是個相當熱門的 Python 資料驗證函式庫。Pydantic 就是以 Type Hint 為基礎,將原本只是註記的動作轉為「強制執行」。
class User:
def __init__(self, id: int, name: str):
self.id = id
self.name = name
user1 = User(id="abc", name=123)
以上程式碼,在 id 的輸入參數部分,即便有標註 int,傳入字串也不會怎麼樣,這可能導致後續執行時的潛在問題。
但如果你用上 Pydantic ,當程式執行時,Pydantice 就會依照提示「強制」擋下不符合的資料:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
user1 = User(id="123", name="Alice")
try:
user2 = User(id="abc", name="Bob")
except Exception as e:
print(e)
上述程式碼中的user1輸入的id會自動轉換:傳入 “123” 會變成整數 123。
而在 try...except區塊的user2則會驗證失敗,因為在id傳入的"abc"無法轉換成int,會拋出 ValidationError。
結語
Type Hint 就像是 Python 程式碼的規格說明書,告訴大家這裡應該放什麼樣的資料。
Pydantic 就是拿著這份說明書的驗貨員,它負責把關程式碼執行時的資料出入,不符合規範的資料它就會擋下來,或是強制整改。
在撰寫程式碼的時候,以上的措施感覺相當繁瑣。但如果你有花心思在「定義資料」上,當未來程式碼發生問題,或是要重構的時候,你會慶幸自己沒有貪圖一時方便,導致錯誤難以追蹤。