Python 的 match 到底能不能当 switch 用?拆开解构、守卫与那个故意不做的 fallthrough
Python 3.10 加了 match 之后,最常见的一句评价是「终于有 switch 了」。这句话方向就错了。
一句话结论:match 不是 switch,它是「结构化模式匹配」——匹配的是数据的形状,不是值;它没有 fallthrough,是语句不是表达式,而且比 dict 分派还快。
以下全部数字来自本机 CPython 3.13.14 实测。
一、它匹配的是形状,不是值
def kind(x):
match x:
case 0: return 'zero'
case [a, b] if a == b: return f'pair-eq:{a}'
case [a, b]: return f'pair:{a},{b}'
case {'k': v, **rest}: return f'map:{v},rest={rest}'
case str() | bytes() as bb: return f'str/bytes:{type(bb).__name__}'
case _: return 'other'
实测输入与输出:
| 输入 | 命中 case | 输出 |
|---|---|---|
0 |
字面量 | zero |
[3, 3] |
序列解构 + 守卫 | pair-eq:3 |
[3, 4] |
序列解构 | pair:3,4 |
{'k': 9, 'z': 1} |
映射 + **rest |
map:9,rest={'z': 1} |
'hi' |
OR 模式 + as 捕获 |
str/bytes:str |
b'yo' |
OR 模式 + as 捕获 |
str/bytes:bytes |
3.14 |
通配符 | other |
switch 能做的事只是第一行。后面五行——把列表拆成两个变量、给 case 加 if 守卫、从字典里挑一个键并把剩下的收进 rest、用 | 把两个类型模式合并再用 as 接住结果——都不是按值比较能表达的。
类模式同样靠「形状」:
class Pt:
__match_args__ = ('x', 'y') # 位置模式的字段顺序
def __init__(self, x, y): self.x, self.y = x, y
def where(p):
match p:
case Pt(0, 0): return 'origin'
case Pt(0, y): return f'on-Y:{y}'
case Pt(x, y): return f'pt:{x},{y}'
实测 where(Pt(0,0))='origin'、where(Pt(0,5))='on-Y:5'、where(Pt(2,3))='pt:2,3'。注意 __match_args__ 必须显式声明,否则 Pt(0, 0) 会抛 TypeError——这是新手最常踩的坑。
二、两个「故意不做」
1. 没有 fallthrough。
def no_fall(x):
r = []
match x:
case 1: r.append('one')
case 2: r.append('two')
case _: r.append('wild')
return r
no_fall(1) # ['one'] ← 不会继续掉进 case 2 / case _
no_fall(9) # ['wild']
C/Java 的 switch 要写 break,漏了就一路滑下去;match 命中第一个 case 就结束。所以不存在「忘写 break」这类 bug,但也别指望靠穿透来共享逻辑——要共享就把条件用 | 合进同一个 case。
2. 是语句,不返回值。
>>> eval('m = match 1:\n case 1: 1')
SyntaxError: invalid syntax
不能 x = match ...,也不能把 match 当表达式塞进 lambda。想拿到结果,得在每个 case 里 return 或者给变量赋值。这一点和 Rust 的 match(表达式)正好相反。
我的理解是:模式匹配天然要绑定一堆变量(a、b、rest、bb),如果做成表达式,这些绑定的作用域和生命周期会很难界定;做成语句,绑定就规规矩矩落在当前局部作用域里。设计上选了简单可预测的一边。
三、性能:比 dict 分派还快
很多人下意识觉得「新语法=更重」。实测打脸:
def by_dict(x):
return {1: 'one', 2: 'two'}.get(x, 'other')
def by_match(x):
match x:
case 1: return 'one'
case 2: return 'two'
case _: return 'other'
timeit 30 万次:
| 写法 | 单次耗时 | 字节码指令数 |
|---|---|---|
by_dict |
258.1 ns | 10 |
by_match |
121.8 ns | 13 |
match 快了约 2.1 倍,尽管字节码条数更多(13 条 vs 10 条)。原因不难猜:by_dict 每次调用都要现场构造一个新字典(两条 LOAD_CONST + BUILD_MAP + dict.get),而 match 的字面量模式编译成了直接的比较跳转,没有堆分配。指令条数多不等于慢,关键看有没有分配。
这给了一个很实用的判断:热路径上的小分派,用 match 比每次现造字典划算;但如果映射表是运行时动态生成、或者分支有几十个,还是老实用字典查表。
四、switch vs match 对照表
| 维度 | C/Java switch |
Python match |
|---|---|---|
| 比较对象 | 单个整数/字符串/枚举的值 | 数据的结构(序列、映射、类实例、类型) |
| 变量绑定 | 不支持 | case [a, b]、as bb、**rest 都能绑 |
| 守卫条件 | 不支持(要写 if 嵌套) | case [a,b] if a == b |
| fallthrough | 默认穿透,需 break |
命中即结束,无穿透 |
| 是否表达式 | 多为语句,Java 14+ 可作表达式 | 纯语句,不能赋值 |
| OR 合并 | 靠穿透共享 | case 1 | 2: |
| 默认分支 | default |
case _(_ 不绑定) |
| 类匹配 | 不支持 | case Pt(0, y) + __match_args__ |
五、排错清单
case x:不是比较变量x:裸名字是捕获模式,会把值绑到x上,永远命中。想比较已存在的变量,得用守卫case _ if v == x,或者用值模式case Color.RED(带点号)。case _必须放最后:它是通配符,放前面后面的 case 全部不可达(CPython 会直接报 SyntaxError)。Pt(0, 0)抛 TypeError:类没写__match_args__,或者位置参数个数和字段数对不上。case [a, b]只匹配长度恰好为 2 的序列:想匹配「至少两个」要写case [a, b, *rest]。字符串不匹配序列模式(str被特殊排除),case [a,b]不会把"hi"拆成h、i。case {'k': v}允许字典有多余键:这是「子集匹配」;要精确匹配得额外加守卫。想收剩余键用**rest。- 别在函数里用
match给全局变量赋值:case 里绑定的名字是局部变量,global声明要照常写。 |两侧必须绑定同名变量:case [a] | [a, a]可以,case [a] | [b]会报 SyntaxError。
六、什么时候该用它
判断标准很简单:如果分支条件里出现了「这个对象是什么形状」「里面有几个元素」「某个键在不在」,就该用 match;如果只是「值等于几」,if/elif 或字典分派反而更清楚。
match 的价值不在替代 switch,而在把「先拆数据、再判断、最后绑定变量」这一串样板代码收进一个语法里。用对了,函数从一层层 isinstance + len() + 索引里解放出来;用错了(拿它当普通 switch),只会多写四个字母还多踩两个坑。

浙公网安备 33010602011771号