跳至主要內容

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.py

mypy.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 = int

Final:不可變的常數

from typing import Final

MAX_RETRIES: Final = 3
API_BASE_URL: Final[str] = "https://api.example.com"

總結

Python 的型別提示系統已經相當成熟,從簡單的 strint 到複雜的 GenericProtocol,都有完善的支援。雖然學習曲線有一定的坡度,但投資在型別標注上的時間能大幅降低未來維護的成本。建議從新專案開始就養成標注型別的習慣,並搭配 mypy 或 pyright 在 CI/CD 流程中進行靜態型別檢查。

分享這篇文章