← 返回题目列表

Python 3.10 的 match-case 是什么?它和 switch 有什么不同?

中等 第 18 / 27 题 更新于 2026/07/31
match结构化模式匹配PEP 634Python 3.10

简化版

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/Falseis
捕获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 检查 + 按属性解构
或 / AScase 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.REDhttp.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 完全一致——那里的 ab 也不是拿旧值去比较,而是被赋值的目标。代价是「拿模块级常量做 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" 不能——strbytesbytearray 被特意排除在序列模式之外。原因很实际:字符串在 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」当组合拳推荐的原因。另外内置类型(intstrlist 等)有特殊支持:它们的单个位置参数表示「匹配对象自身」,所以 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 时虽然没进这个分支,但 xy 两个变量已经被赋值了(Python 没有块级作用域,它们在整个函数里可见)。实践中不要依赖这个副作用来传值——它让代码难以推理,而且如果后面某个 case 恰好也用了同名变量,会读到上一个失败 case 留下的值。同样值得注意的是:守卫表达式里可以调用函数,但每尝试一个 case 就可能执行一次,所以守卫里别放有副作用或很慢的调用(写日志、发请求),把它挪到 case 体内更安全。

八、加强记忆

match-case(Python 3.10+,PEP 634)叫「结构化模式匹配」,它不是 switch 的替代品,而是「按数据的形状匹配 + 匹配即解构」case {"type": "click", "pos": [x, y]} 一行同时完成「是不是字典 / type 等不等于 click / pos 是不是二元序列 / 把两个值绑给 xy」,替代掉一大坨 isinstance + in + len + 下标访问 的样板。必须背下的第一条铁律:模式里的裸名字永远是「捕获」而不是「比较」——case MAX: 会匹配一切并把输入覆盖到 MAX 上(和解包赋值里的名字是同一套逻辑),要比常量得用带点的值模式Color.REDStatus.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