Python 3.10 的 match-case 是什么?它和 switch 有什么不同?
简化版
match-case(Python 3.10+,PEP 634)叫「结构化模式匹配」,不是 C/Java 那种只能比相等的 switch——它匹配的是「数据的形状」,并在匹配成功的同时把里面的部分「解构」出来绑定到变量。比如 case {"type": "user", "id": int(uid)} 一句话同时完成三件事:判断是不是字典、判断 type 字段等于 "user"、判断 id 是整数并把它绑给 uid。支持的模式有:字面量模式(case 200:)、捕获模式(case x: 匹配任意值并绑定)、通配符(case _: 兜底)、序列模式(case [a, b, *rest]:)、映射模式(case {"k": v}:,只需部分匹配,多余的键不影响)、类模式(case Point(x=0, y=y):)、或模式(case 401 | 403:)、AS 模式(case [Point()] as pts:),还能用 if 加守卫条件(case [x, y] if x == y:)。最大的坑:case CONST: 里如果 CONST 是个裸名字,它不是在和常量比较,而是「捕获一切并把值赋给 CONST」——想比较常量必须写成带点的形式(case Color.RED:)或用 if 守卫。没有 fall-through(不用写 break),没有匹配到且没有 case _ 时静默跳过(不报错)。核心记忆:match 是「按形状匹配 + 解构赋值」,裸名字永远是捕获而不是比较。
详细版
八种模式速查:
| 模式 | 写法 | 说明 |
|---|---|---|
| 字面量 | case 200: / case "ok": / case None: | 用 == 比较;None/True/False 用 is |
| 捕获 | case x: | 匹配任意值并绑定给 x(放最后当兜底) |
| 通配符 | case _: | 匹配任意值但不绑定,标准兜底写法 |
| 值(点号) | case Color.RED: / case status.OK: | 带点才是比较常量 |
| 序列 | case [a, b]: / case [x, *rest]: | 匹配列表/元组等序列(str、bytes 不算) |
| 映射 | case {"k": v}: | 部分匹配:有 k 就行,多余键无所谓 |
| 类 | case Point(x=0, y=y): | isinstance 检查 + 按属性解构 |
| 或 / AS | case 401 | 403: / case Point() as p: | 多选一 / 给整体起名 |
from dataclasses import dataclass
# ① 最基础:字面量 + 或模式 + 守卫 + 兜底
def http_hint(code):
match code:
case 200 | 201 | 204:
return "成功"
case 301 | 302:
return "重定向"
case c if 400 <= c < 500: # 守卫条件:先捕获 c,再用 if 过滤
return f"客户端错误 {c}"
case _: # 兜底(没有它且没匹配上 → 静默什么都不做)
return "其他"
# ② 结构化解构:一句话完成"判断形状 + 取值"
def handle(event):
match event:
case {"type": "click", "pos": [x, y]}: # 嵌套:字典里套列表
return f"点击 ({x}, {y})"
case {"type": "key", "code": str(k)}: # 顺带做类型校验
return f"按键 {k}"
case {"type": t, **rest}: # **rest 收集其余键
return f"未知事件 {t},附加字段 {list(rest)}"
case _:
return "非法事件"
print(handle({"type": "click", "pos": [3, 4], "ts": 99})) # 点击 (3, 4)(多余的 ts 不影响)
# ③ 类模式(配合 dataclass 最顺)
@dataclass
class Point:
x: int
y: int
def where(p):
match p:
case Point(x=0, y=0): return "原点"
case Point(x=0, y=y): return f"Y 轴上,y={y}"
case Point(x=x, y=0): return f"X 轴上,x={x}"
case Point(): return "平面上某处"
case _: return "不是 Point"
print(where(Point(0, 5))) # Y 轴上,y=5
# ④ ★头号陷阱:裸名字是"捕获"不是"比较"★
STATUS_OK = 200
def bad(code):
match code:
case STATUS_OK: # ✗ 这不是"等于 200",是"匹配任意值并赋给 STATUS_OK"
return "永远走这里"
# 实际上 Python 会直接报 SyntaxError:name capture 'STATUS_OK' makes remaining patterns unreachable
# 但如果它不是最后一个 case、或名字没被后面用到,就可能悄悄吃掉所有输入
class Status: # ✓ 正确做法一:包一层,用带点的名字
OK = 200
def good(code):
match code:
case Status.OK: # ✓ 带点 = 值模式 = 真正的相等比较
return "OK"
case c if c == STATUS_OK: # ✓ 正确做法二:用守卫
return "OK"
case _:
return "其他"
# ⑤ 序列模式的坑:字符串不是序列模式的匹配对象
match "ab":
case [a, b]: print("不会走这里") # str/bytes/bytearray 被特意排除
case str(): print("字符串") # ✓ 走这里
⚠️ 三个必须记住的语义:① 裸名字 = 捕获。
case X:里只要X是个不带点的普通名字,Python 就把它当成「捕获模式」——匹配任何值并把值绑定给X,而不是拿X的当前值去比较。这是 PEP 634 最受诟病但也最一致的设计(因为模式里的名字统一表示「要绑定的目标」,和解包赋值a, b = t里的a, b一个道理)。想比较常量必须用带点的形式(Color.RED、http.OK)或加守卫(case c if c == X:)。好在编译器会在「裸名字不是最后一个 case」时报SyntaxError: makes remaining patterns unreachable,帮你挡住一部分。② 映射模式是部分匹配、序列模式是完全匹配:case {"a": 1}能匹配{"a":1,"b":2}(多余的键无所谓),但case [1, 2]匹配不了[1,2,3](长度必须一致,除非写*rest)。③ 没有 fall-through、没有强制穷尽:匹配到第一个就执行完退出(不用break),但如果一个 case 都没匹配上又没写case _:,match会静默地什么都不做(不像某些语言会报错),这是漏兜底时的隐蔽 bug 来源。
完整版教学
一、它不是 switch:从「比相等」到「比形状」
C/Java 的 switch 能力:只能拿一个值和若干常量比相等
switch (code) { case 200: ...; break; case 404: ...; break; }
→ 想判断"这是个含有 type 字段且 pos 是二元列表的字典"?switch 无能为力
Python 的 match 能力:匹配"数据的结构(形状)"并同时解构
match event:
case {"type": "click", "pos": [x, y]}:
... # 到这里 x、y 已经被绑好了
一条 case 同时做了 5 件事:
① 是字典吗? (类型检查)
② 有 "type" 键吗? (结构检查)
③ "type" 的值 == "click" 吗? (值比较)
④ 有 "pos" 键、且它是长度为 2 的序列吗?(嵌套结构检查)
⑤ 把两个元素分别绑给 x、y (解构绑定)
用 if 写等价逻辑:
if (isinstance(event, dict) and event.get("type") == "click"
and isinstance(event.get("pos"), (list, tuple)) and len(event["pos"]) == 2):
x, y = event["pos"]
...
→ 5 行、3 次重复访问 event、还容易漏掉 isinstance 检查
与 switch 的其他差别:
fall-through:C 要写 break,match ★没有穿透★(匹配一个就结束)
穷尽性: match ★不强制★兜底,没匹配上就静默跳过(不报错!)
绑定: switch 不绑定任何东西,match 是"匹配即解构"
守卫: match 可以 case pattern if 条件:(switch 没有)
理解 match 的第一步是别把它当 switch 的替代品。switch 解决的是「一个值 vs 一串常量」的分派,用 Python 的 if-elif 或字典分派就能干得很好,为这点事新加语法完全不值得。match 真正解决的是处理「结构化数据」时那一大坨 isinstance + in + len + 下标访问 的样板代码——解析 JSON 事件、处理 AST 节点、写解释器、匹配协议报文,这些场景下你要反复确认「这个数据长成什么样」,然后再把里面的字段取出来。match 把「检查形状」和「取出内容」合并成了一行声明式的写法,可读性提升是数量级的。记住它的三个非 switch 特性:没有 fall-through(不用写 break)、不强制穷尽(没匹配上静默跳过,必须自己写 case _:)、匹配即绑定。
二、捕获模式:case X: 为什么不是比较
核心规则一句话:模式里的"裸名字"永远表示"把匹配到的值绑定到这个名字"
case x: 匹配任意值,绑给 x
case [a, b]: 匹配二元序列,绑给 a、b
case {"k": v}: 取 "k" 的值绑给 v
case Point(x=px): 取属性 x 绑给 px
所以:
MAX = 100
match n:
case MAX: ✗ 不是"n == 100",而是"匹配一切并把 n 赋给 MAX"!
print("满了") # 任何 n 都会走这里,而且 MAX 被改成了 n 的值
为什么这么设计(不是拍脑袋):
和解包赋值保持一致 —— a, b = t 里的 a、b 也是"目标"而不是"要比较的值"
如果裸名字表示比较,那 case x: 就没法写"捕获任意值"了,
而"捕获"在结构化匹配里比"和某个变量比较"常见得多
编译器的保护(帮你挡住一部分):
match n:
case MAX: # 捕获模式吃掉一切
...
case 200: # 永远到不了
...
→ SyntaxError: name capture 'MAX' makes remaining patterns unreachable
★ 但如果 case MAX 恰好是最后一个 case,就不报错、静默生效 → 真实 bug
三种正确的"和常量比较"写法:
① 带点的值模式(推荐):
class Status: OK = 200
case Status.OK: ✓ (只要带 . 就是取值比较)
case Color.RED: ✓ (枚举天然带点,最常用)
case http.HTTPStatus.OK: ✓
② 直接写字面量:
case 200: ✓
③ 守卫条件:
case c if c == MAX: ✓ (先捕获,再用 if 过滤)
顺带:通配符 _ 是"匹配但不绑定"
case _: 匹配一切、不产生变量(这是唯一被特殊对待的名字)
这是 match 唯一一条「必须靠背」的规则,也是面试最爱问的点:模式里的裸名字一律是「绑定目标」,不是「比较对象」。设计上它和解包赋值 a, b = t 完全一致——那里的 a、b 也不是拿旧值去比较,而是被赋值的目标。代价是「拿模块级常量做 case」这种直觉写法会静默失效:case MAX: 会吃掉所有输入,还顺手把 MAX 覆盖成了输入值。编译器只在「捕获模式后面还有其他 case」时报 SyntaxError(因为后面的分支永远不可达),如果它恰好是最后一个 case,就不报错、静默生效。所以工程上的对策很明确:常量要么放进枚举或类里用带点的形式引用,要么老实写字面量,要么用守卫条件。
三、序列模式与映射模式:两套不同的匹配规则
序列模式 —— 完全匹配(长度必须对得上)
case []: 空序列
case [x]: 恰好 1 个元素
case [x, y]: 恰好 2 个
case [x, *rest]: 至少 1 个,其余收进 rest(rest 是 list)
case [*_, last]: 至少 1 个,只关心最后一个
case (x, y): ★和 [x, y] 完全等价★(括号/方括号在模式里不区分元组和列表)
★ 谁算"序列":list、tuple、range、以及注册为 collections.abc.Sequence 的类型
★ 谁★不算★:str、bytes、bytearray、以及 iterator/generator
match "ab":
case [a, b]: ... # ✗ 不会匹配(否则 "ab" 会被拆成字符,几乎总是误伤)
match (x for x in [1,2]):
case [a, b]: ... # ✗ 生成器不匹配(模式匹配不会消费迭代器)
映射模式 —— 部分匹配(多余的键不影响)
case {"type": "user"}: 只要有 type=="user" 就匹配,其他键随便
case {"id": int(i)}: 有 id 且是 int,绑给 i
case {}: ★匹配任何映射★(不是"空字典"!)← 高频误解
case {"a": 1, **rest}: rest 收集其余键值(rest 是 dict)
★ 没有 "**_" 这种写法(**_ 是禁止的,因为映射本来就允许多余键)
★ 键必须是字面量或值模式(不能是捕获模式:case {k: v} ✗ 语法错误)
为什么两套规则不一样:
序列的"长度"是它的本质结构 → 长度不同就是形状不同
映射的"多几个键"通常无关紧要(JSON 报文加个字段不该让匹配失败)
→ 这是照顾真实使用场景的取舍,不是不一致
嵌套算例:
event = {"type": "click", "pos": [3, 4], "ts": 1699999999}
case {"type": "click", "pos": [x, y]} → 匹配成功,x=3, y=4(ts 被忽略)
case {"type": "click", "pos": [x]} → 失败(pos 长度不是 1)
case {"type": "click", "pos": [x, y, z]}→ 失败(长度不是 3)
序列模式和映射模式的规则故意不一样,理解了动机就不会记混:序列的长度就是它的形状,[x, y] 明确表示「恰好两个」,想放宽就写 *rest;而映射多几个键通常无关紧要——JSON 报文里多了个 ts 字段不该让你的处理逻辑失效,所以映射模式是部分匹配。由此产生一个高频误解:case {}: 不是「匹配空字典」,而是「匹配任何映射」(因为它没有提出任何键的要求),要判断空字典得用守卫 case d if not d:。另外两个易忘点:(x, y) 和 [x, y] 在模式里完全等价(模式匹配不区分列表和元组,写哪个都一样),以及 str/bytes 被特意排除在序列模式之外(否则 case [a, b] 会把 "ab" 拆成两个字符,几乎总是误伤),生成器同样不匹配(模式匹配不会去消费迭代器)。
四、类模式:match_args 决定位置参数
类模式的两种写法:
① 关键字形式(推荐,永远可用):
case Point(x=0, y=y): # 检查 isinstance + p.x == 0 + 绑定 p.y
② 位置形式(需要类定义了 __match_args__):
case Point(0, y): # 位置参数按 __match_args__ 的顺序对应属性
__match_args__ 是什么:
class Point:
__match_args__ = ("x", "y") # 声明"位置模式时,第 1 个对应 x、第 2 个对应 y"
def __init__(self, x, y): self.x, self.y = x, y
★ @dataclass 会★自动生成★ __match_args__(按字段声明顺序)→ 所以 dataclass 和 match 是绝配
★ 普通类没写 __match_args__ 时用位置形式 → TypeError: Point() accepts 0 positional sub-patterns
内置类型的特殊支持(只接受一个位置参数,匹配"自身"):
case int(n): 是 int,绑给 n ← 常用作"类型校验 + 绑定"
case str() | bytes(): 是字符串或字节串
case list([a, b]): 是 list 且有两个元素 ← 类模式套序列模式
内置类型 bool/bytearray/bytes/dict/float/frozenset/int/list/set/str/tuple
都支持这种"单个位置参数 = 匹配整个对象"的写法
类模式做的三步检查:
① isinstance(目标, 类) ← 支持继承!子类实例也能匹配父类模式
② 每个关键字/位置对应的属性存在且子模式匹配
③ 绑定
典型用法:用类模式替代一长串 isinstance 分支(解释器/AST 处理的标准写法)
match node:
case BinOp(left=l, op="+", right=r): return ev(l) + ev(r)
case BinOp(left=l, op="*", right=r): return ev(l) * ev(r)
case Num(value=v): return v
case _: raise ValueError(node)
类模式是 match 在真实项目里最有价值的部分:它把「isinstance 判断 + 取属性 + 比较属性值」压成一行,特别适合处理AST、消息对象、状态机事件这类「一族相关的类」。要点有三个:① 关键字形式 Point(x=0, y=y) 永远可用,位置形式 Point(0, y) 需要类定义 __match_args__(一个属性名元组),否则报 TypeError;② @dataclass 会自动生成 __match_args__(按字段声明顺序),所以 dataclass 和 match 是天生一对,这也是现代 Python 里「数据类 + 模式匹配」被当作组合拳的原因;③ 内置类型支持 int(n)、str(s) 这种单参数写法,语义是「是这个类型,并把整个对象绑给 n」——它看起来像构造函数调用,实际是「类型校验 + 绑定」,是模式里做类型检查最简洁的写法。另外类模式的第一步是 isinstance,所以子类实例能匹配父类模式,写继承体系时要注意把更具体的子类 case 放在前面。
五、守卫、或模式、AS 模式与绑定规则
守卫(guard):模式匹配成功后再加一道 if
case [x, y] if x == y: # 先解构,再要求两元素相等
print("对角线上的点")
case c if c in ALLOWED: # 用捕获 + 守卫来"和变量比较"
...
★ 执行顺序:先匹配模式(会产生绑定)→ 再算守卫 → 守卫为假则整个 case 失败、继续下一个
★ 守卫为假时,★绑定已经发生了★(变量已被修改,只是没进这个分支)—— 别依赖这点
或模式 |:多个模式选一个
case 401 | 403 | 407: # 常量多选
case Point(x=0) | Point(y=0): # 结构多选
★ 铁律:每个分支必须绑定★完全相同★的变量名,否则 SyntaxError
case [x] | [x, y]: ✗ alternative patterns bind different names
case [x] | [x, _]: ✓ 两边都只绑 x
AS 模式:给整体(或子模式)起个名字
case [Point(x=0), Point()] as pair: # pair 绑到整个列表
case {"data": {"id": int()} as info}: # info 绑到内层字典
case (int() | float()) as num: # 常见:类型多选 + 拿到值
组合示例(一个真实的报文分派):
match msg:
case {"op": "sub", "topics": [*ts]} if len(ts) <= 10:
subscribe(ts)
case {"op": "pub", "topic": str() as t, "body": body}:
publish(t, body)
case {"op": "ping"} | {"op": "heartbeat"}:
pong()
case {"op": op}:
raise ValueError(f"未知操作 {op}")
case _:
raise TypeError("报文不是字典")
绑定的可见性:
绑定的变量在 match 语句★之后仍然有效★(不是块级作用域)
match p:
case Point(x=x): pass
print(x) # ✓ 能访问(Python 没有块级作用域)
这三个特性负责补齐表达力。守卫(case 模式 if 条件:)用来表达「结构对了但还有额外约束」——比如两元素相等、列表长度上限、值在白名单里;它也是「和变量比较」的标准解法(case c if c == MAX:)。要注意执行顺序:模式先匹配(绑定已经发生)→ 再算守卫 → 守卫为假就换下一个 case,也就是说守卫失败时变量其实已经被改了,别依赖这个副作用。或模式 | 有一条硬性约束:每个分支必须绑定完全相同的变量名,否则直接 SyntaxError(因为进入分支后编译器要保证所有变量都有定义)。AS 模式解决「我既要检查内部结构、又要拿到整体」的需求,case (int() | float()) as num: 是它最常见的用法。最后记住一个作用域事实:Python 没有块级作用域,case 里绑定的变量在 match 之后依然可访问——好处是方便,坏处是没匹配上的分支留下的绑定可能造成混淆。
六、什么时候该用 match:和 if-elif / 字典分派的取舍
三种分派方式的对比:
┌──────────────┬────────────────┬──────────────┬────────────────────┐
│ 维度 │ if-elif │ 字典分派 │ match-case │
├──────────────┼────────────────┼──────────────┼────────────────────┤
│ 简单值分派 │ 清晰 │ ★最佳★(O(1))│ 可以,但杀鸡用牛刀 │
│ 结构化数据 │ 样板代码爆炸 │ 做不到 │ ★最佳★ │
│ 类型分派 │ isinstance 链 │ 需手工建表 │ ★最佳★(类模式) │
│ 分支可动态增减│ 不方便 │ ★最佳★(改表) │ 不行(写死在语法里) │
│ 性能 │ O(n) 次比较 │ O(1) 哈希 │ O(n) 次尝试匹配 │
│ 版本要求 │ 任意 │ 任意 │ ★3.10+★ │
└──────────────┴────────────────┴──────────────┴────────────────────┘
选择准则:
① 只是"值 == 常量"的分派,且分支多 → 字典分派(handlers[op]())最快最灵活
② 需要判断"数据长什么样"(嵌套字典/列表/多种类型) → match
③ 一族类的分派(AST、事件、状态机) → match 的类模式
④ 只有两三个分支 → if-elif 就够,别引入新语法
⑤ 要兼容 3.9 及以下 → 只能 if-elif(★match 是语法,3.9 上整个文件都 import 不了★)
性能提醒:
match 不是跳转表!它是"从上到下依次尝试每个 case",本质是 O(n)
(CPython 有一些针对字面量的优化,但不要指望它像 C 的 switch 那样 O(1))
→ 有几十上百个"值 → 处理函数"的分支,字典分派仍然是正解
可读性提醒:
一个 case 的模式别嵌套超过 2~3 层,太深的模式比 if 还难读
✗ case {"a": {"b": {"c": [{"d": x}]}}}: # 已经在炫技了
✓ 先取出 inner = data.get("a", {}).get("b"),再对 inner 做 match
选型的核心问题是:你分派的依据是「一个值」还是「一种形状」。分派依据只是一个值(op == "add"、code == 404)且分支很多时,字典分派 handlers[op]() 才是最优解——O(1) 哈希查找、分支可以运行时动态增减、还能拆到多个模块注册;match 在这种场景既没有性能优势(它是从上到下依次尝试,本质 O(n),不是 C 那样的跳转表),也丢失了灵活性(分支写死在语法里)。而当分派依据是「数据的形状」——嵌套的 JSON、多种类型混杂的输入、一族类的实例——match 就是压倒性的选择,它把十几行 isinstance + get + len 变成一眼能读懂的声明式模式。最后两个现实约束:match 是语法特性,3.9 及以下连文件都 import 不了(编译期就报 SyntaxError);模式嵌套别超过两三层,太深的模式比原来的 if 还难读,不如先把内层数据取出来再匹配。
记忆钩子:「
match-case(3.10+)不是 switch,是『结构化模式匹配』——按『数据的形状』匹配,并在匹配成功的同时解构绑定:case {"type":"click","pos":[x,y]}一行完成『是不是字典 + type 等不等于 click + pos 是不是二元序列 + 把两个值绑给 x,y』。★头号铁律:模式里的裸名字永远是捕获(匹配一切并赋值给它),不是比较——case MAX:会吃掉所有输入还顺手覆盖 MAX;要比常量就用带点的形式(Color.RED)、字面量或守卫(case c if c == MAX:)。★两套不同的匹配规则:序列模式完全匹配(长度必须一致,除非*rest;且 str/bytes/生成器不算序列),映射模式部分匹配(多余的键无所谓,所以case {}:是『匹配任何映射』而不是『空字典』)。★类模式做 isinstance + 属性解构,位置形式要靠__match_args__(dataclass 自动生成,两者是绝配);int(n)/str(s)是『类型校验 + 绑定』。★或模式|的各分支必须绑定完全相同的名字。★没有 fall-through(不写 break),但也不强制穷尽——没匹配上就静默跳过,兜底case _:要自己写。选型:值分派用字典(O(1)、可动态增减),形状分派才用 match(它是 O(n) 依次尝试,不是跳转表)。」
七、常见误区与追问
- 误区:
case CONST:是在和常量CONST比较。 这是最经典的坑:模式里的裸名字一律是「捕获模式」——匹配任意值并把值绑定给这个名字,所以MAX = 100之后写case MAX:意思是「匹配一切,并把输入赋给MAX」,任何输入都会走这个分支,MAX本身还被覆盖掉了。设计上它和解包赋值a, b = t一致(那里的名字也是目标而非比较对象)。编译器只在「捕获模式后面还有其他 case」时报SyntaxError: makes remaining patterns unreachable,如果它恰好是最后一个 case 就静默生效。正确写法三选一:带点的值模式(case Status.OK:、枚举case Color.RED:,只要带.就是取值比较)、直接写字面量(case 200:)、或用守卫(case c if c == MAX:)。 - 误区:
case {}:匹配空字典。 它匹配任何映射——映射模式是「部分匹配」,{}表示「我对键没有任何要求」,所以{"a":1,"b":2}也能匹配上,把它放在前面会吃掉后面所有的字典分支。判断空字典要用守卫:case d if not d:,或case {} if not d:这类写法(配合 AS 模式case {} as d if not d:)。对称地,序列模式的case []:确实只匹配空序列(序列模式是完全匹配,长度必须一致)——两套规则不同,这正是最容易记混的地方:序列比长度,映射不比键数。 - 误区:没有匹配到任何 case 会报错。 不会——
match在所有 case 都不匹配时静默地什么都不做,直接执行match语句之后的代码,不抛异常、不打日志。这和某些语言(如 Rust 的match)强制穷尽完全不同,是漏兜底时非常隐蔽的 bug 来源:函数走完match后返回None,调用方拿到None才在很远的地方炸掉。工程实践是永远显式写兜底:要么case _: raise ValueError(f"未处理的输入: {x!r}"),要么case _: return default——即使你「确信」所有情况都覆盖了,也应该把「不可能发生」的分支写出来。 - 误区:
match像 C 的 switch 一样是跳转表,比 if-elif 快。match的执行模型是从上到下依次尝试每个 case,本质是 O(n)——CPython 对字面量模式有一些优化,但不存在 C 那种基于跳转表的 O(1) 分派。所以「几十上百个『值 → 处理函数』的分支」这种场景,字典分派handlers[op]()依然是最优解(O(1) 哈希查找、分支能在运行时动态注册/替换、还能拆到多个模块)。match的价值在表达力(形状匹配 + 解构)而非速度;把它当性能优化手段用是选错了工具。另外把最可能命中的 case 放前面对match是有意义的,这也侧面说明它是顺序尝试。 - 误区:
case [a, b]:能匹配字符串"ab"。 不能——str、bytes、bytearray被特意排除在序列模式之外。原因很实际:字符串在 Python 里恰好也是序列,如果允许匹配,case [a, b]:就会把"ab"拆成两个字符,而绝大多数情况下这是误伤(你想匹配的是「两个元素的列表」,不是「两个字符的字符串」)。同理生成器和迭代器也不匹配序列模式,因为模式匹配不应该去消费一个一次性的迭代器(那会产生副作用且不可回退)。想匹配字符串就用类模式case str() as s:,想匹配生成器就先list()落地。 - 追问:
__match_args__是干什么的,为什么 dataclass 和 match 是绝配? 类模式有两种写法:关键字形式case Point(x=0, y=y):(永远可用,显式指定属性名)和位置形式case Point(0, y):(更简洁)。位置形式需要知道「第 1 个位置参数对应哪个属性」,这个映射就存在类属性__match_args__里——一个属性名的元组,如__match_args__ = ("x", "y")。普通类如果没定义它,用位置形式会直接报TypeError: Point() accepts 0 positional sub-patterns。而@dataclass会按字段声明顺序自动生成__match_args__,NamedTuple同样自带,所以这些「数据载体类」天生就支持简洁的位置模式,配合match处理事件、AST、状态机时代码非常干净——这就是现代 Python 把「dataclass + match」当组合拳推荐的原因。另外内置类型(int、str、list等)有特殊支持:它们的单个位置参数表示「匹配对象自身」,所以case int(n):的语义是「是 int 就把整个对象绑给 n」,而不是取某个属性。 - 追问:或模式
|为什么要求各分支绑定相同的变量名? 因为进入 case 体之后,编译器必须保证每一个可能被使用的变量都已经有值。如果允许case [x] | [x, y]:,那么当输入是单元素列表时y就没有被绑定,case 体里一旦用到y就会NameError——而这种错误只在特定输入下才出现,属于最难排查的一类。Python 干脆在编译期就拒绝:SyntaxError: alternative patterns bind different names。绕开的办法是让两边绑定一致(case [x] | [x, _]:,用通配符占位表示「有这个元素但我不关心」),或者干脆拆成两个 case 分别处理。同理,case _:里的_是唯一被特殊对待的名字——它是「匹配但不绑定」的通配符,所以在或模式里用_占位不会引入绑定不一致的问题。 - 追问:守卫条件失败时,模式已经产生的绑定还在吗?会不会有副作用? 会还在。执行顺序是:先匹配模式(此时绑定已经发生)→ 再计算守卫表达式 → 守卫为假则这个 case 整体失败、继续尝试下一个 case。也就是说
case [x, y] if x > y:在x <= y时虽然没进这个分支,但x、y两个变量已经被赋值了(Python 没有块级作用域,它们在整个函数里可见)。实践中不要依赖这个副作用来传值——它让代码难以推理,而且如果后面某个 case 恰好也用了同名变量,会读到上一个失败 case 留下的值。同样值得注意的是:守卫表达式里可以调用函数,但每尝试一个 case 就可能执行一次,所以守卫里别放有副作用或很慢的调用(写日志、发请求),把它挪到 case 体内更安全。
八、加强记忆
match-case(Python 3.10+,PEP 634)叫「结构化模式匹配」,它不是 switch 的替代品,而是「按数据的形状匹配 + 匹配即解构」:case {"type": "click", "pos": [x, y]} 一行同时完成「是不是字典 / type 等不等于 click / pos 是不是二元序列 / 把两个值绑给 x、y」,替代掉一大坨 isinstance + in + len + 下标访问 的样板。必须背下的第一条铁律:模式里的裸名字永远是「捕获」而不是「比较」——case MAX: 会匹配一切并把输入覆盖到 MAX 上(和解包赋值里的名字是同一套逻辑),要比常量得用带点的值模式(Color.RED、Status.OK)、字面量或守卫(case c if c == MAX:);编译器只在捕获模式后面还有 case 时报错,它是最后一个 case 时静默生效。第二条:序列模式完全匹配、映射模式部分匹配——case [1,2] 匹配不了三元素列表(除非 *rest),而 case {"a":1} 能匹配含更多键的字典,因此 case {}: 是「匹配任何映射」而非「空字典」;另外 str/bytes/生成器不算序列模式的目标,(x,y) 与 [x,y] 在模式里完全等价。第三条:类模式 = isinstance + 属性解构,位置形式依赖 __match_args__(dataclass / NamedTuple 自动生成,所以它们和 match 是绝配),int(n)/str(s) 表示「类型校验并把对象整体绑定」。第四条:或模式 | 的各分支必须绑定完全相同的名字(否则编译期 SyntaxError),守卫在模式匹配成功之后才求值(此时绑定已发生)。第五条:没有 fall-through(不用写 break),但也不强制穷尽——一个都没匹配上时静默跳过、不报错,所以兜底 case _: 必须自己写,最好在里面 raise。选型上:纯值分派用字典(O(1)、可动态注册),形状/类型分派才用 match(它是从上到下依次尝试的 O(n),不是跳转表);且它是语法特性,3.9 及以下整个文件都无法 import。