Python Type Hints 完整指南:讓程式碼更易讀更安全
Python Type Hints 完整指南:讓程式碼更易讀更安全
Python 3.5 引入了 Type Hints(型別提示),而在 Python 3.10、3.11、3.12 版本中,型別系統持續得到大幅強化。雖然 Python 本質上仍是動態型別語言,型別提示不會在執行期強制執行,但搭配靜態分析工具(如 mypy、pyright)和現代 IDE,型別提示能大幅提升程式碼的可讀性、可維護性,並在開發階段及早發現潛在的錯誤。
基礎型別標注
變數與函式參數
# 變數型別標注
name: str = "Alice"
age: int = 30
score: float = 9.5
is_active: bool = True
# 函式參數與回傳值
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
def calculate_area(width: float, height: float) -> float:
return width * height
# 沒有回傳值
def log_message(msg: str) -> None:
print(f"[LOG] {msg}")集合型別
from typing import List, Dict, Set, Tuple # Python 3.8 以前需要這樣
# Python 3.9+ 可以直接使用內建型別
# 串列
names: list[str] = ["Alice", "Bob", "Charlie"]
scores: list[int] = [95, 87, 92]
# 字典
user_data: dict[str, int] = {"age": 30, "score": 95}
config: dict[str, str | int] = {"name": "app", "port": 8080}
# 集合
unique_tags: set[str] = {"python", "backend", "api"}
# 元組(固定長度)
point: tuple[float, float] = (1.0, 2.0)
rgb: tuple[int, int, int] = (255, 128, 0)
# 元組(可變長度)
coordinates: tuple[float, ...] = (1.0, 2.0, 3.0, 4.0)Optional 與 Union
from typing import Optional, Union
# Optional[X] 等同於 X | None
def find_user(user_id: int) -> Optional[dict]:
# 可能回傳 dict 或 None
...
# Python 3.10+ 可以用 | 語法
def find_user_v2(user_id: int) -> dict | None:
...
# Union 型別
def process(value: Union[str, int]) -> str:
return str(value)
# Python 3.10+ 簡化寫法
def process_v2(value: str | int) -> str:
return str(value)TypedDict:字典的型別安全
from typing import TypedDict
class UserProfile(TypedDict):
id: int
name: str
email: str
age: int
class PartialUserProfile(TypedDict, total=False):
# total=False 表示所有欄位都是可選的
name: str
email: str
def update_profile(user_id: int, data: PartialUserProfile) -> UserProfile:
...dataclass 與型別提示
dataclass 和型別提示是絕配:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class Article:
title: str
slug: str
content: str
author: str
created_at: datetime = field(default_factory=datetime.now)
tags: list[str] = field(default_factory=list)
is_published: bool = False
def publish(self) -> None:
self.is_published = True
# 使用
article = Article(
title="Python 型別提示指南",
slug="python-type-hints",
content="...",
author="Alice"
)Protocol:結構化子型別
Protocol 讓你定義鴨子型別(duck typing)的介面,不需要繼承即可滿足:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None:
...
def get_area(self) -> float:
...
class Circle:
def __init__(self, radius: float):
self.radius = radius
def draw(self) -> None:
print(f"Drawing circle with radius {self.radius}")
def get_area(self) -> float:
import math
return math.pi * self.radius ** 2
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def draw(self) -> None:
print(f"Drawing {self.width}x{self.height} rectangle")
def get_area(self) -> float:
return self.width * self.height
# Circle 和 Rectangle 都滿足 Drawable Protocol
def render_all(shapes: list[Drawable]) -> None:
for shape in shapes:
shape.draw()
print(f"Area: {shape.get_area():.2f}")
shapes: list[Drawable] = [Circle(5.0), Rectangle(3.0, 4.0)]
render_all(shapes)Generic 泛型
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("Stack is empty")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("Stack is empty")
return self._items[-1]
def is_empty(self) -> bool:
return len(self._items) == 0
# 使用泛型
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value: int = int_stack.pop() # 型別安全地取得 int
str_stack: Stack[str] = Stack()
str_stack.push("hello")Callable 型別
from typing import Callable
# 接受一個函式作為參數
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(x: int) -> int:
return x * 2
result = apply_twice(double, 3) # 結果:12
# 更複雜的 Callable
Handler = Callable[[str, dict[str, str]], None]
def register_route(path: str, handler: Handler) -> None:
...Literal 型別
from typing import Literal
def set_direction(direction: Literal['north', 'south', 'east', 'west']) -> None:
...
def get_status() -> Literal['active', 'inactive', 'pending']:
return 'active'
# 有效呼叫
set_direction('north')
# mypy 會報錯
set_direction('up') # Error: Argument 1 to "set_direction" has incompatible type "Literal['up']"使用 mypy 進行靜態分析
# 安裝
pip install mypy
# 執行檢查
mypy your_file.py
# 嚴格模式
mypy --strict your_file.pymypy.ini 設定範例:
[mypy]
python_version = 3.12
strict = True
ignore_missing_imports = True
[mypy-third_party_lib.*]
ignore_missing_imports = True實用技巧
型別別名
from typing import TypeAlias
# Python 3.12 新語法
type UserId = int
type UserName = str
type UserRecord = dict[UserId, UserName]
# Python 3.10+ 的寫法
UserId: TypeAlias = intFinal:不可變的常數
from typing import Final
MAX_RETRIES: Final = 3
API_BASE_URL: Final[str] = "https://api.example.com"總結
Python 的型別提示系統已經相當成熟,從簡單的 str、int 到複雜的 Generic、Protocol,都有完善的支援。雖然學習曲線有一定的坡度,但投資在型別標注上的時間能大幅降低未來維護的成本。建議從新專案開始就養成標注型別的習慣,並搭配 mypy 或 pyright 在 CI/CD 流程中進行靜態型別檢查。
分享這篇文章