Python 型別提示 (Type Hints) 與 Typing
Python 是一種動態型別 (Dynamically Typed) 的語言,這意味著變數的型別是在執行時期 (Runtime) 決定的,我們不需要(也無法強制)在宣告變數時指定型別。
然而,隨著專案規模變大,缺乏型別資訊會讓程式碼難以閱讀和維護。為了解決這個問題,Python 3.5 引入了型別提示 (Type Hints)。
為什麼要使用型別提示?
- 增加可讀性:明確指出函式需要什麼參數、會回傳什麼,讓其他開發者(包括未來的自己)更容易看懂程式碼。
- IDE 支援:現代的編輯器 (如 VS Code, PyCharm) 可以利用型別資訊提供更好的自動完成 (Auto-completion) 和錯誤提示。
- 減少 Bug:搭配靜態型別檢查工具 (如
mypy),可以在執行前就發現型別錯誤。
基本語法
變數型別註釋
使用 變數: 型別 的語法:
name: str = "Alice"
age: int = 30
is_active: bool = True
height: float = 1.75
也可以只宣告不賦值(但通常不建議這樣做,除非在類別屬性中):
score: int
# ... 稍後再賦直
score = 100
函式型別註釋
我們可以標註 參數型別 和 回傳值型別 (使用 ->):
def greeting(name: str) -> str:
return "Hello, " + name
def add(x: int, y: int) -> int:
return x + y
容器型別 (Collection Types)
這部分在 Python 3.9 前後有一些變化。
Python 3.9+ (推薦)
從 Python 3.9 開始,我們可以直接使用內建的容器類別 (如 list, dict, set) 來作為型別提示:
# 一個包含整數的列表
scores: list[int] = [10, 20, 30]
# 一個 key 為字串,value 為整數的字典
user_ages: dict[str, int] = {
"Alice": 30,
"Bob": 25
}
# 一個包含字串的集合
tags: set[str] = {"python", "coding"}
# 一個包含特定三個元素的元組
point: tuple[int, int, int] = (1, 2, 3)
Python 3.8 及更早版本
在舊版 Python 中,你需要從 typing 模組引入對應的大寫類別:
from typing import List, Dict, Set, Tuple
scores: List[int] = [10, 20, 30]
user_data: Dict[str, str] = {"name": "Alice"}
typing 模組的特殊型別
對於更複雜的型別需求,我們需要使用 typing 模組。
Any (任意型別)
Any 表示該變數可以是任何型別。這等於是告訴檢查工具:「這裡不要檢查型別」。
from typing import Any
def print_anything(value: Any) -> None:
print(value)
print_anything(123)
print_anything("Hello")
Union (聯合型別)
當一個變數可能是多種型別之一時使用。
Python 3.10+ (推薦寫法,使用 |):
def process_id(user_name: str, user_id: int | str):
# user_id 可以是整數或字串
print(f"User: {user_name}, ID: {user_id}")
舊版寫法 (使用 Union):
from typing import Union
def process_id(user_id: Union[int, str]):
pass
Optional (可選型別)
Optional[T] 等同於 Union[T, None] 或 T | None,表示該值可能是型別 T,或是 None。這常用於有預設值的參數。
# Python 3.10+ 推薦寫法
def find_user(user_id: int) -> dict | None:
if user_id == 1:
return {"name": "Admin"}
return None
# 舊版寫法
from typing import Optional
def find_user_legacy(user_id: int) -> Optional[dict]:
...
Literal (字面值型別)
有時候我們希望變數不只是某種型別,還必須是特定的幾個值。
from typing import Literal
def set_status(status: Literal["pending", "approved", "rejected"]):
print(f"Status set to: {status}")
set_status("approved") # OK
# set_status("deleted") # 靜態檢查工具會報錯
TypeAlias (型別別名)
如果某個型別組合很長且多次使用,我們可以使用 TypeAlias 定義一個別名,增加可讀性。
from typing import TypeAlias
# 定義一個複雜的型別別名
Coordinates: TypeAlias = list[tuple[float, float]]
def process_path(path: Coordinates):
for x, y in path:
print(f"Moving to {x}, {y}")
Annotated (附加資訊型別)
Annotated 是 Python 3.9 引入的一個非常強大的功能。它允許你在型別提示中附加中繼資料 (Metadata)。
這個語法本身不會改變型別檢查的結果,但許多框架(如 FastAPI 和 Pydantic)會利用這些中繼資料來進行資料驗證或依賴注入。
from typing import Annotated
# 基本語法:Annotated[原始型別, 中繼資料1, 中繼資料2, ...]
# 以下範例表示 userId 是 int,且附加了說明文字
UserId: TypeAlias = Annotated[int, "這是使用者的唯一識別碼"]
def get_user(uid: UserId):
return f"User {uid}"
在 FastAPI 中的應用:
from fastapi import Query
from typing import Annotated
# 表示 q 是一個字串,且長度必須在 3 到 10 之間
def search(q: Annotated[str | None, Query(min_length=3, max_length=10)] = None):
return q
TypedDict (強型別字典)
普通的 dict[str, Any] 無法限制字典裡有哪些 Key。TypedDict 可以讓你定義字典的固定結構。
from typing import TypedDict
class UserProfile(TypedDict):
name: str
age: int
email: str | None
# 建立符合結構的字典
user: UserProfile = {
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
Protocol (結構化子型別)
Protocol 類似於 Java 或 Go 中的 Interface(介面)。它定義了一種契約,只要某個類別實踐了 Protocol 定義的方法,它就被視為該型別,而不需要顯式繼承。
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("Drawing a circle")
class Square:
def draw(self) -> None:
print("Drawing a square")
def render(shape: Drawable):
shape.draw()
render(Circle()) # OK,因為 Circle 有 draw 方法
render(Square()) # OK
Self (自身型別)
在類別方法中,如果回傳值是「類別自己」,Python 3.11 引入了 Self 讓定義更簡單。
from typing import Self
class Builder:
def set_name(self, name: str) -> Self:
self.name = name
return self
builder = Builder().set_name("MyBuilder")
Callable (可呼叫物件)
當參數是需要傳入一個函式時使用。
語法:Callable[[參數型別1, 參數型別2], 回傳型別]
from typing import Callable
def apply_func(x: int, y: int, func: Callable[[int, int], int]) -> int:
return func(x, y)
def multiply(a: int, b: int) -> int:
return a * b
print(apply_func(5, 3, multiply)) # 15
Final (常數)
用來標示變數不應該被重新賦值(類似其他語言的 const)。
from typing import Final
PI: Final[float] = 3.14159
# PI = 3.14 # 靜態檢查工具會報錯
泛型 (Generics) 與 TypeVar
有時候我們希望函式的輸入型別和輸出型別是有關聯的,這時可以使用 TypeVar。
from typing import TypeVar
T = TypeVar('T')
def get_first_element(items: list[T]) -> T:
return items[0]
# 如果傳入 list[int],回傳就是 int
print(get_first_element([1, 2, 3]))
# 如果傳入 list[str],回傳就是 str
print(get_first_element(["a", "b", "c"]))
靜態型別檢查工具 (mypy)
正如前面提到,Python 執行時不會檢查這些提示。要真正發揮型別提示的威力,你需要使用靜態檢查工具。最常用的是 mypy。
安裝:
pip install mypy
執行檢查:
mypy script.py
如果有型別不符的地方,mypy 就會報錯,幫助你在寫程式的階段就抓出 Bug。