## 引言
上一篇我们把 `if / elif / else` 的语法与执行流程讲透了:自上而下测试条件、命中即止、`else` 兜底。这套机制覆盖了绝大多数「按条件分岔」的需求,但它有一个结构性短板:当分支判据不是「数值大小」而是「值长什么样」——比如把一个枚举值映射到行为、把一个元组按形状拆解——`if` 链会写得很啰嗦。Python 3.10(2021 年 10 月发布)为此引入了一等公民:`match` 语句,全称**结构化模式匹配**(structural pattern matching)。
本篇是第三章第二篇,阅读前提:掌握上一篇的 `if / elif / else`,以及第 2 章的元组与列表基本操作(`(x, y)` 这种打包/解包在第 6 章会系统展开,本文只用最简单的一层解包)。本篇含金量在于回答三个问题:`match` 的语法是什么;它和 `if` 链到底区别在哪(为什么它被称为「模式匹配」而不是「语法糖 switch」);什么时候该用它、什么时候该退回 `if` 链。循环、推导式等后续篇章一律不展开。
先给一个版本提醒:`match` 是 3.10 的新语法,**Python 3.9 及以下的解释器会直接报 `SyntaxError`**。在 3.10+ 里,`match` 与 `case` 是**软关键字**(soft keyword)——只在语句的特定位置被当作关键字,平时仍可当变量名用(本文第 7 个代码块会验证)。这篇所有代码块的运行环境是 Python 3.12,行为与 3.10/3.11 完全一致。
## 概念与原理
### 从「判断条件」到「匹配形状」:范式转变
`if` 链问的是「这个条件成立吗」,每个分支携带一个任意复杂的布尔表达式,条件之间是**计算**关系。`match` 问的是「这个值长什么样」,每个分支写一个**模式**(pattern),模式与值之间是**形状**关系:字面量模式比较值是否相等,捕获模式把值装进新变量,通配符 `_` 对形状不挑剔。条件的计算是每步求值,而模式的匹配是「一眼看出形状」。正因为它描述的是数据的结构,它才叫模式匹配——这个编程范式源自函数式语言(Miranda、Haskell、OCaml 等),Python 3.10 的官方 PEP 634(规范)、PEP 635(动机与原理)、PEP 636(教程)三件套把它正式引入。
另一个与 `if` 的重大区别:**`match` 的主语(subject)只求值一次**。`if` 链里每个条件中的表达式各自求值,而 `match 主语:` 后主语只被求值一次,交给后续所有 case 去匹配——这避免了「同一个表达式被反复计算」的啰嗦与不一致风险。
### 语法骨架与执行流程
`match` 的整体结构是:`match 主语:` 之后缩进并列多个 `case 模式:` 块,每个 case 可以选配 `if 守卫条件`,通常最后跟一个 `case _:` 兜底。执行流程与 `if` 链惊人地相似:从第一个 case 开始,自上而下把主语与每个模式做匹配测试;**第一个匹配成功的 case 执行它的块,然后整个 `match` 语句立即结束**——后续 case 不再测试。所有 case 都不匹配时,什么也不做(没有兜底时)。注意三点与 C/Java 的 `switch` 的根本差异。其一,**没有穿透(fall-through)**:每个 case 块执行完就退出 `match`,不需要也不允许写 `break`(写了反而是语法错误)。其二,case 的模式在**编译期**就要合法(比如 OR 模式里绑定名字不一致直接报错),而不是等到运行期。其三,匹配是「自上而下第一个命中」,与 `elif` 链同一语义,这一点很容易被当成「dict 分发」而忽略顺序的意义。
### 五种基础模式(本篇范围)
按官方 PEP 636 的术语,本篇覆盖五种模式:
- **字面量模式**(literal pattern):`case "red":`、`case 200:`,用值相等(`==`)比较;
- **捕获模式**(capture pattern):`case other:`,只要先前的模式没命中,它就一定命中,并把主语**绑定**到新变量 `other` 上——注意它不比较、只绑定;
- **通配符模式**(wildcard pattern):`case _:`,匹配一切**且不绑定**任何变量,专职兜底;
- **或模式**(OR pattern):`case 1 | 2 | 3:`,竖线连接多个子模式,逐个尝试,任一命中即该 case 命中;
- **守卫**(guard):`case s if s >= 90:`,在模式之后加 `if` 条件,模式先匹配、守卫再判断,守卫为假则该 case 不命中,继续尝试下一个 case。
另外预告**序列模式**(sequence pattern)的雏形:`case (x, y):` 可以把元组/列表按元素拆开匹配并同时捕获元素——这是「结构化」二字的精髓,本文用一个坐标例子热身,系统展开留在后续篇章。
### 与 if 链的取舍:什么时候用哪个
两种语句的适用边界可以一句话概括:**判据是数值范围、多个条件的逻辑组合(且/或/非)时用 `if` 链;判据是值的「形状」——等于哪几个离散值、是什么结构、要按结构拆包——时用 `match`**。`if` 链的每个条件是完整的布尔表达式,可以表达 `0 < x < 10 or x == 100` 这类动态计算;`match` 的模式是「声明式的形状描述」,无法表达「大于 90 且小于 100」这种需要计算的条件(只能靠守卫补救)。反过来,五个离散值的分发用 `if` 链需要写五个 `==`,`match` 一行 `case "a" | "b" | "c" | "d" | "e":` 搞定。实现层面,CPython(3.10+)会把 `match` 编译成类似决策树的字节码,常规场景下运行开销与手写 `if` 链相当——**选择的主要依据是可读性,不是性能**。
## 操作与实现
先看最朴素的用法:把离散状态翻译成行为(交通灯):
```python
light = "red"
match light:
case "red":
print("停车") # 停车
case "yellow":
print("准备")
case "green":
print("通行")
```
主语 `"red"` 与第一个 case 的字面量模式 `"red"` 相等,命中,块执行,`match` 结束。把 `light` 改成 `"green"`,前两个 case 测试失败、第三个命中。注意没有 `break`——这是与 C 系 `switch` 的最大区别。
OR 模式与通配符配合,把多个值归并到同一分支:
```python
day = "Sat"
match day:
case "Mon" | "Tue" | "Wed" | "Thu" | "Fri":
print("工作日")
case "Sat" | "Sun":
print("周末") # 周末
case _:
print("无效的星期")
```
`case _:` 承接了所有未知输入,保证程序对脏数据也安全。若删掉这一行,`day = "?"` 时整个 `match` 静默无事发生——这正是无兜底 case 的行为。
捕获模式:把「其他情况」的值装进变量,用于后续使用(命令分发器):
```python
command = "status"
match command:
case "start":
print("启动服务")
case "stop":
print("停止服务")
case other:
print(f"未知命令:{other}") # 未知命令:status
```
`case other:` 匹配一切并把主语装进 `other`——它和 `case _:` 的唯一区别就是「绑不绑定变量」。**注意:`other` 是新建的局部变量,与之前在别处赋值的 `other` 没有关系,模式里的捕获名字总是重新绑定**。
守卫:模式先匹配、守卫再放行,守卫为假则继续尝试下一个 case:
```python
score = 83
match score:
case s if s >= 90:
print("A")
case s if s >= 80:
print("B") # B
case s if s >= 60:
print("C")
case _:
print("不及格")
```
83 通过第一个守卫(83 >= 90 为假)失败,转入第二个 case:`s` 捕获 83,守卫 `83 >= 80` 为真,命中。这个例子同时也说明了守卫的局限:它不能代替「区间」判断的自然表达——四行 `if score >= 90: ...` 链在这里其实更直白(这正是上一篇学过的东西不该被丢掉的原因)。
序列模式的雏形——按结构拆包(坐标分类):
```python
point = (10, -3)
match point:
case (0, 0):
print("原点")
case (x, 0):
print(f"在 x 轴上,x={x}")
case (0, y):
print(f"在 y 轴上,y={y}")
case (x, y):
print(f"普通点 ({x}, {y})") # 普通点 (10, -3)
```
`case (x, 0):` 要求主语是「两个元素、第二个元素为 0」的结构,并把第一个元素绑定给 `x`。`(10, -3)` 依次不满足前三个模式(第二个元素不是 0),落入第四支 `(x, y)`——两项都捕获。这一行模式完成了「判结构 + 拆包 + 绑定」三件事,`if` 链要写三行。序列模式的完整规则(任意长度、嵌套、映射模式、类模式等)留给后续篇章。
同一个任务,`if` 链与 `match` 的对照(HTTP 状态码分类):
```python
status = 200
# 写法一:if 链(3.x 通用)
if status == 200:
print("OK")
elif status == 301 or status == 302:
print("重定向")
else:
print("其他")
# 写法二:match(3.10+)
match status:
case 200:
print("OK") # OK
case 301 | 302:
print("重定向")
case _:
print("其他")
```
两种写法输出相同(本机 3.12 验证),但语义密度不同:写法二中「301 或 302」是模式本身的一部分,代码量更少、意图更直白;而「200-299 全算成功」这种范围判断,`match` 就束手无策——只能写 `case s if 200 <= s < 300:`,反而绕。
最后一个验证:`match` 是软关键字,平时可以当普通名字用:
```python
match = 42 # 软关键字:做变量名完全合法
print(match + 1) # 43
```
`fresh = "case"` 也是一样。但注意:**不要在 `match` 语句内部把 `case` 当变量名**——`case` 在模式位置有特殊含义,那样的代码几乎必然写成合法但诡异的捕获模式(见陷阱一)。
## 易错点与陷阱
### 陷阱一:把常量名当字面量写进 case——它其实是捕获
这是模式匹配最经典的坑:`if color == RED:` 里 `RED` 是「读一个变量的值去比较」,但 `case RED:` 里裸名字 `RED` 是**捕获模式**——它不读外部的 `RED`,而是匹配一切并重新绑定 `RED`。实测(Python 3.12.10)先看它「能跑」时的样子:
```python
RED = "FF0000"
color = "00FF00"
match color:
case RED: # 裸名字:是捕获,不是常量比较
print("命中:这是捕获,不是常量比较") # 命中:这是捕获,不是常量比较
print(RED) # 00FF00:全局 RED 已被覆盖
```
`color` 明明是绿色,`case RED:` 却无条件命中,还把顶层变量 `RED` 覆盖成了 `"00FF00"`。更糟的是,若在它后面再写别的 case,新一点的 CPython(本文实测的 3.12.10 即如此)会直接编译期拦截:`SyntaxError: name capture 'RED' makes remaining patterns unreachable`——捕获匹配一切,后续 case 永远不可达,实现就此报错。而在没有这项编译期检查的旧版本(如 3.10/3.11)上,同样的代码会静默地按捕获执行,绿色被输出成红色,且查不出原因。想按常量比较,必须用点号限定属性(`case 模式.Color.RED:` 这类类模式,后续篇章展开)或直接写字面量。这条陷阱从 PEP 636 官方教程起就被反复强调,是评审高频送分题。
### 陷阱二:OR 模式里绑定不一致的名字,编译期直接报错
`case 1 | x:` 合法吗?不合法。OR 模式的每个分支会分别绑定自己的捕获变量,Python 要求**所有分支绑定同一组名字**(严格说名字集必须相同),否则报 `SyntaxError: alternative patterns bind different names`。`case 1 | x:` 里第一个分支不绑定任何名字、第二个绑定 `x`,名字集不一致,编译即失败。设计原因:如果允许,`case 1 | x:` 命中 1 时 `x` 就没有定义,运行时语义会分裂。这是少见的「编译期拦截」陷阱,遇到先检查 OR 模式的绑定对称性。
### 陷阱三:守卫失败 ≠ 跳过整个 match——只是跳过当前 case
`case n if n > 0:` 中守卫为假时,控制权交还给 `match`,继续尝试**下一个 case**,不是退出 `match`。想表达「正整数、负整数、零」三分类时,守卫顺序颠倒会互相踩踏。还有一个语言规范没有承诺、但 CPython 3.12 实测成立的行为:**捕获发生在守卫评估之前**——`case n if n > 0:` 即使守卫失败,`n` 也已经被绑定为当前主语;整个 `match` 结束后该变量照样残留。这对「在 case 里绑定、在 match 外使用」的代码是劫持:你以为 `n` 只在命中时定义,实际它几乎总被定义。请把 `match` 当封闭的判断单元,需要结果的场景显式在命中分支里赋值,不要在 `match` 体外读 case 内绑定的变量。
### 陷阱四:全员不命中且没有兜底 case
`match` 与 `if` 链一样:所有 case 都不中且没有 `case _:` 时,整个语句静默结束。若后续代码使用了只在某个 case 里绑定的变量,会 `NameError`——这和上一篇「忘记 else 变量悬空」是同一个坑的结构化版本。分发类代码请固定写好兜底 case;「模式匹配失败」本身也是一种需要处理的结果(要么打印警告,要么抛异常,异常第 7 章再讲)。
## 小结
`match`(Python 3.10+)是流程控制的第二种分支语句:主语只求值一次,case 自上而下测试模式,第一个命中的 case 执行后整个语句结束,无穿透、无需 break。本篇五种模式是全部地图的第一块:字面量、捕获、通配符 `_`、或模式 `|`、守卫 `if`;序列模式以坐标例子热身。取舍口诀:**离散值/形状分派用 match,范围与复合逻辑用 if 链**,二者输出等价时读代码的人才是裁判。四大陷阱:裸常量名其实是捕获、OR 绑定名字必须一致、守卫失败只是跳过当前 case、没兜底时静默无事。下一篇进入循环:`while` 条件循环——「命中即止」的分支逻辑变成「条件反复执行」的循环逻辑,流程控制开始有「重复」这个维度。
## 练习与思考题
1. 不运行代码,写出 `match 5: case 5: print("A") case _: print("B")` 与 `match 5: case x: print(x) case 5: print("C")` 各自的输出,再运行验证——第二段代码揭示了什么?
2. 用 `match` 重写「星期判断」:周一至周五「工作日」、周六周日「周末」、其余输入「无效」,要求包含 OR 模式与通配符。再想想:若把 `case "Mon" | "Tue"`: 写成 `case ("Mon") | ("Tue")`:,行为有区别吗?(提示:括号不影响字面量。)
3. 下面的守卫代码想判断「正数」,但它有 bug:`match v: case n if n > 0: print("正数") case n: print("非正数")`——请问 `v = 0` 与 `v = -1` 时分别输出什么?把第二个 case 改成 `case n if n <= 0:` 后行为如何?为什么第二种写法更保险?
4. 思考题:查阅 PEP 635 的「动机」,回答为什么设计者不给 `case` 加 `break`/`fall-through` 语义?「match 在此如 if 链命中即止」这个设计与 C 的 switch 相比,对代码可维护性意味着什么?