2021:結構化模式比對與 Python 3.10——match/case 與聯合型別#
事件#
2021 年 10 月,Python 3.10 發佈,主要新特性:
- 結構化模式比對(PEP 634/635/636):
match ... case——帶捕捉與解構的 switch - 聯合型別新語法(PEP 604):
int | str取代Union[int, str] - 括號化上下文管理器(PEP 617 之後的語法):多個
with用括號一行搞定 - 更精確的錯誤訊息:指出確切欄位的 traceback
為何重要#
match/case是 Python 30 年來最大的語法新增:這不是 C 的 switch——它是結構化模式比對:可以比對序列、映射、類別結構並同時解構捕捉(case Point(x=0, y=y):)。這是 Python 吸收 ML/Haskell/Erlang(1960–1990)與 Rust(2015)傳統的跨界設計。int | str:PEP 604 用數學的聯合符號|取代typing.Union[...],型別語法更接近型別理論的原貌。- 錯誤訊息大改進:3.10 的 traceback 標出確切出錯的表示式——教學與除錯體驗質變。
理論與實用原因#
- 理論原因(match):
- 模式比對源自 ML(1978) 與 Haskell(1990) 的代數資料類型(ADT)解構、Erlang(1986) 的訊息比對、Rust(2015) 的
match。理論上,「比對形狀並解構」比「層層 if + 手動索引取值」更安全(不會取錯位)、更接近資料結構的定義本身。 - Python 沒有 ADT,但 3.10 的 match 支援序列/映射/類別模式——用「結構」而非「型別」比對,是 Python 鴨子型別哲學下的獨特妥協;exhaustiveness 檢查則交由型別檢查器(mypy/pyright)補足。
- 理論原因(
X | Y):型別理論中聯合型別(sum type)的標準記法就是A + B/A | B;typing.Union[A, B]是 Java 泛型式的囉嗦寫法。PEP 604 讓型別語法回到理論原貌,且|是真運算子(可在執行期對型別物件求值)。 - 實用原因:JSON/訊息/設定解析(形狀多變的資料)是 match 的最大場景;
Union的冗長讓型別提示在實務中敬而遠之,|大幅降低摩擦。
彌補了什麼缺陷#
- match/case 彌補了「多分支 if/elif 難以表達資料形狀、解構與比對分離、易取錯索引」的缺陷。
X | Y彌補了typing.Union「冗長、不像型別理論、嵌套時可讀性崩壞」的缺陷。- 精確錯誤訊息彌補了「traceback 不指出確切表示式、教學除錯困難」的缺陷。
程式範例#
結構化模式比對(PEP 634)——前後對比:
# 3.10 之前:層層 if + 手動索引取值,易取錯位
def handle(cmd):
if isinstance(cmd, list) and len(cmd) >= 3 and cmd[0] == "move":
x, y = cmd[1], cmd[2] # 取錯索引就出錯
return f"move {x} {y}"
elif cmd == "quit":
return "bye"
return "unknown"
# 3.10 之後:比對形狀並解構——源自 ML/Haskell/Rust 的 match
def handle(cmd):
match cmd:
case ["move", x, y]: # 序列模式:比對 + 解構
return f"move {x} {y}"
case ["zoom", factor] | ["scale", factor]: # 或模式
return f"zoom {factor}"
case {"action": "login", "user": name}: # 映射模式
return f"login {name}"
case Point(x=0, y=y): # 類別模式:比對屬性並捕捉
return f"on y-axis at {y}"
case _:
return "unknown"
聯合型別 X | Y(PEP 604)——型別語法回到理論原貌:
# 3.10 之前:typing.Union 冗長,嵌套時可讀性崩壞
from typing import Union, Optional
def find(id: int) -> Optional[Union[str, int]]: ...
# 3.10 之後:數學的聯合符號 |,是真運算子
def find(id: int) -> str | int | None: ...
括號化上下文管理器與精確錯誤訊息:
# 多個 with 一行搞定(3.9 之前要反括號或巢狀)
with (open("a.txt") as a, open("b.txt") as b):
...
# 3.10 的 traceback 標出確切出錯的表示式:
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
# print(count + name) ← 確切指出是這個 + 出錯