AIGC标识 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__

五、排错清单

  1. case x: 不是比较变量 x:裸名字是捕获模式,会把值绑到 x 上,永远命中。想比较已存在的变量,得用守卫 case _ if v == x,或者用值模式 case Color.RED(带点号)。
  2. case _ 必须放最后:它是通配符,放前面后面的 case 全部不可达(CPython 会直接报 SyntaxError)。
  3. Pt(0, 0) 抛 TypeError:类没写 __match_args__,或者位置参数个数和字段数对不上。
  4. case [a, b] 只匹配长度恰好为 2 的序列:想匹配「至少两个」要写 case [a, b, *rest]。字符串不匹配序列模式(str 被特殊排除),case [a,b] 不会把 "hi" 拆成 h、i。
  5. case {'k': v} 允许字典有多余键:这是「子集匹配」;要精确匹配得额外加守卫。想收剩余键用 **rest。
  6. 别在函数里用 match 给全局变量赋值:case 里绑定的名字是局部变量,global 声明要照常写。
  7. | 两侧必须绑定同名变量:case [a] | [a, a] 可以,case [a] | [b] 会报 SyntaxError。

六、什么时候该用它

判断标准很简单:如果分支条件里出现了「这个对象是什么形状」「里面有几个元素」「某个键在不在」,就该用 match;如果只是「值等于几」,if/elif 或字典分派反而更清楚。

match 的价值不在替代 switch,而在把「先拆数据、再判断、最后绑定变量」这一串样板代码收进一个语法里。用对了,函数从一层层 isinstance + len() + 索引里解放出来;用错了(拿它当普通 switch),只会多写四个字母还多踩两个坑。

posted @ 2026-11-06 09:19  小小张说故事  阅读(5)  评论(0)    收藏  举报