Python 基础体系 · 第 11/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python 结构化模式匹配:match、case、守卫、绑定与陷阱

Python 的 match 语句在 Python 3.10 引入,Python 3.14 中仍然属于标准语言语法。它解决的不是“把多个 if 换成另一种写法”,而是把值的分类、结构检查、数据提取和条件筛选组合进一种声明式控制流中。(docs.python.org)

例如,下面的代码同时完成了三件事:

  1. 判断输入是否是一个字典;
  2. 判断字典中的 "type" 是否为 "user"
  3. 在匹配成功后,把 "id""name" 绑定到局部变量。
def describe(message):
    match message:
        case {"type": "user", "id": user_id, "name": name}:
            return f"user {user_id}: {name}"
        case {"type": "system", "level": level}:
            return f"system level={level}"
        case _:
            return "unknown message"

理解 match 的关键,不是记住若干特殊符号,而是区分四个概念:

  • 模式:描述输入值应当具有什么结构;
  • 匹配:模式是否适用于当前值;
  • 绑定:匹配过程中把部分值赋给名称;
  • 守卫:模式成功后,继续执行一个普通布尔表达式。

1. match 的执行模型

一个 match 语句的基本形式是:

match subject:
    case pattern_1:
        block_1
    case pattern_2 if guard_2:
        block_2
    case pattern_3:
        block_3

形式化地,可以把它看成:

match(S,Pi,Gi)\operatorname{match}(S, P_i, G_i)

其中:

  • SSmatch 后面的主题值,即 subject;
  • PiP_i 是第 iicase 的模式;
  • GiG_i 是可选守卫;
  • Pi(S)P_i(S) 的结果是模式成功或失败;
  • 当模式成功且守卫为真时,执行对应代码块。

运行过程按以下顺序进行:

  1. 先计算主题表达式;
  2. 从第一个 case 开始,依次尝试模式;
  3. 如果模式失败,跳到下一个 case
  4. 如果模式成功,执行其中的名称绑定;
  5. 如果存在守卫,再计算守卫表达式;
  6. 守卫为真时执行代码块;
  7. 守卫为假时继续尝试后面的 case
  8. 一旦某个 case 被选中,后续 case 不再尝试。

官方语言参考明确规定,模式失败时,某些子模式可能已经成功,但不能依赖失败匹配后的变量绑定状态;具体行为可以随实现和优化而变化。(docs.python.org)

1.1 主题表达式只产生一个值

value = (1, 2)

match value:
    case (1, 2):
        print("matched")

如果 match 后面出现逗号,Python 会按照普通元组表达式规则构造元组:

match 1, 2:
    case (1, 2):
        print("matched")

上面的主题值也是 (1, 2),而不是先匹配 1、再匹配 2。主题表达式的计算结果始终是一个对象。(docs.python.org)

1.2 matchif 的根本区别

if 只需要一个布尔条件:

if message.get("type") == "user":
    ...

match 的模式则可以同时表达:

case {"type": "user", "id": user_id}:

这相当于要求:

message 是一个映射"type"messagemessage["type"]=="user""id"messageuser_id=message["id"]\begin{aligned} &message \text{ 是一个映射}\\ &"type" \in message\\ &message["type"] == "user"\\ &"id" \in message\\ &user\_id = message["id"] \end{aligned}

因此,match 不是单纯的相等比较,而是结构匹配加值提取


2. matchcase 是软关键字

matchcasesoft keyword,软关键字。它们只有在特定语法位置才具有关键字含义,在普通代码中仍然可以作为变量名。(docs.python.org)

match = "ordinary variable"
case = 42

print(match, case)

但以下代码中,matchcase 会被解析为模式匹配语法:

match value:
    case 1:
        ...

这与 iffor 等硬关键字不同。软关键字的设计目的,是尽量减少新语法对旧代码的破坏。


3. 模式不是表达式

这是理解结构化模式匹配最重要的前提之一。

在普通表达式中:

x = 1
result = x + 2

x 表示读取变量 x 的值。

但在模式中:

match value:
    case x:
        ...

x 不是读取已有变量,而是一个 捕获模式,表示:

无论 value 是什么,都匹配成功,并把它绑定给 x

因此:

def classify(value):
    match value:
        case x:
            return f"matched: {x}"

这个函数对任何输入都会返回匹配结果。

case x 等价于:

x = value

而不是:

value == x

捕获模式总是成功,并遵循赋值表达式相关的作用域规则:在函数中通常会成为最近外层函数作用域的局部变量,除非存在适用的 globalnonlocal 声明。(docs.python.org)


4. 字面量模式:真正执行比较的模式

字面量模式包括数字、字符串、NoneTrueFalse

def status_text(status):
    match status:
        case 200:
            return "ok"
        case 404:
            return "not found"
        case None:
            return "missing"
        case _:
            return "other"

对于普通数字和字符串,匹配逻辑近似于:

subject == literal

NoneTrueFalse 使用身份判断语义,即分别对应单例对象。(docs.python.org)

match value:
    case None:
        print("value is None")
    case True:
        print("value is True")
    case False:
        print("value is False")

4.1 True1 和布尔值的陷阱

由于 Python 中:

True == 1

结果为真,很多人会误以为:

match value:
    case 1:
        ...

一定会区分 1True

实际上,字面量模式对普通值使用相等比较,而布尔字面量使用身份比较。更稳妥的做法是显式安排分支:

def classify(value):
    match value:
        case True:
            return "boolean true"
        case 1:
            return "integer one"
        case _:
            return "other"

分支顺序仍然重要。匹配成功后,后面的分支不会再被尝试。


5. 通配模式 _ 与捕获模式的区别

match value:
    case _:
        return "anything"

_ 是通配模式:

  • 总是匹配成功;
  • 不绑定任何名称;
  • 通常作为最后的兜底分支。
match value:
    case _:
        print(value)  # 这里不能通过 _ 取得主题值

_ 在模式位置是通配符,但在其他位置仍然是普通标识符:

_ = "ordinary name"

match 10:
    case _:
        print("wildcard")

case 代码块中:

_ = "ordinary name"

match 10:
    case _:
        print(_)  # 输出 ordinary name

这里的 _ 不再处于模式位置,而是普通变量读取。官方文档特别强调,_ 只在模式中具有通配含义,在主题表达式、守卫和代码块中仍是普通标识符。(docs.python.org)

如果既想匹配任意值,又想保存整个对象,应使用捕获模式:

match value:
    case item:
        print(item)

6. 值模式:为什么常量必须写成限定名称

值模式使用带点号的名称,例如:

class Protocol:
    HTTP = "http"
    HTTPS = "https"

match scheme:
    case Protocol.HTTP:
        print("HTTP")
    case Protocol.HTTPS:
        print("HTTPS")

Protocol.HTTP 是值模式,因为它是一个限定名称。解释器会按照普通名称解析规则找到它,然后使用相等比较判断主题值是否相等。(docs.python.org)

6.1 常见错误:直接写常量名

HTTP = "http"

match scheme:
    case HTTP:
        print("HTTP")

这段代码中的 HTTP 是捕获模式,不是值模式。它会无条件匹配,并把 scheme 绑定到 HTTP

如果需要匹配常量,应使用:

class Protocol:
    HTTP = "http"

match scheme:
    case Protocol.HTTP:
        ...

或者使用枚举成员:

from enum import Enum


class Protocol(Enum):
    HTTP = "http"
    HTTPS = "https"


def describe(protocol):
    match protocol:
        case Protocol.HTTP:
            return "HTTP"
        case Protocol.HTTPS:
            return "HTTPS"
        case _:
            return "unknown"

这里的 Protocol.HTTP 是值模式,而不是捕获模式。

6.2 EnumIntEnum 的差异

普通 Enum 成员不等于其底层值:

from enum import Enum, IntEnum


class Color(Enum):
    RED = 1


class Number(IntEnum):
    ONE = 1


print(Color.RED == 1)   # False
print(Number.ONE == 1)  # True

IntEnum 成员同时也是整数,可以参与整数运算,并且与整数相等;普通 Enum 则不会自动表现为底层值。(docs.python.org)

因此,以下代码的匹配对象不同:

match value:
    case Color.RED:
        print("Enum member")
    case 1:
        print("integer one")

对于协议解析、状态机和命令分派,应明确决定匹配的是枚举成员还是序列化后的字符串、整数。


7. 序列模式:按位置匹配结构

序列模式使用方括号或带逗号的圆括号:

def parse_point(value):
    match value:
        case [x, y]:
            return x, y
        case _:
            raise ValueError("expected a 2-item sequence")

以下对象可以匹配:

parse_point([10, 20])
parse_point((10, 20))

序列模式的基本条件是:

  1. 主题值必须是序列;
  2. strbytesbytearray 不作为序列模式匹配;
  3. 没有星号模式时,长度必须完全相等;
  4. 子模式按从左到右的顺序匹配对应元素。(docs.python.org)

例如:

match [10, 20]:
    case [x, y]:
        print(x, y)

执行过程是:

主题值:[10, 20]
长度检查:2 == 2,成功
第一个子模式 x:绑定 x = 10
第二个子模式 y:绑定 y = 20
整个模式:成功

7.1 为什么字符串不会被拆开匹配

match "ab":
    case [first, second]:
        print(first, second)
    case _:
        print("not a sequence pattern match")

输出:

not a sequence pattern match

虽然字符串支持索引和长度操作,但结构化模式匹配特意排除了字符串、字节串和字节数组,避免 "ab" 被意外解释为两个字符的结构。

7.2 星号模式

星号模式可以吸收任意数量的中间元素:

match [1, 2, 3, 4]:
    case [first, *middle, last]:
        print(first, middle, last)

输出:

1 [2, 3] 4

绑定规则是:

first = 1
last = 4
middle = [2, 3]

星号模式最多出现一次,并且绑定结果始终是列表:

match [1, 2]:
    case [*items]:
        print(items)  # [1, 2]

星号模式还可以不绑定名称:

match [first, _, last]:
    case [first, *_rest, last]:
        ...

但是一个序列模式中不能出现两个星号:

match value:
    case [*head, *tail]:  # SyntaxError
        ...

7.3 固定长度与可变长度

match value:
    case [x, y]:
        ...

要求:

len(value)=2len(value) = 2

而:

match value:
    case [x, *rest]:
        ...

只要求:

len(value)1len(value) \geq 1

因为 x 至少需要一个元素,rest 可以为空列表。

例如:

def split_first(value):
    match value:
        case [first, *rest]:
            return first, rest
        case _:
            raise ValueError("empty sequence")
split_first([10, 20, 30])
# (10, [20, 30])

split_first([10])
# (10, [])

8. 映射模式:匹配键,而不是匹配完整字典

映射模式使用花括号:

def read_user(data):
    match data:
        case {"id": user_id, "name": name}:
            return user_id, name
        case _:
            raise ValueError("invalid user object")

它要求:

  1. 主题值是映射;
  2. "id""name" 都存在;
  3. 对应值分别匹配子模式;
  4. 其他额外键不会导致失败。

因此:

read_user({
    "id": 7,
    "name": "Ada",
    "role": "admin",
})

可以成功。映射模式并不要求字典的键集合与模式完全相等。(docs.python.org)

如果需要获取剩余字段,可以使用 **

def split_user(data):
    match data:
        case {"id": user_id, **extra}:
            return user_id, extra
        case _:
            raise ValueError("missing id")
split_user({"id": 7, "name": "Ada", "role": "admin"})
# (7, {"name": "Ada", "role": "admin"})

**extra 必须位于映射模式末尾,并且一个映射模式最多有一个双星号模式。(docs.python.org)

8.1 映射模式不会触发 __missing__

映射模式通过映射的双参数 get() 语义检查键值。它要求键已经存在,不会依赖 __missing__() 或通过 __getitem__() 动态创建缺失值。(docs.python.org)

from collections import defaultdict


data = defaultdict(lambda: "created")

match data:
    case {"missing": value}:
        print(value)
    case _:
        print("not matched")

即使 defaultdict 具有默认工厂,缺失键也不会因为模式匹配而自动生成。

8.2 映射模式中的键必须是字面量或值模式

键位置不能写任意捕获模式:

match data:
    case {key: value}:  # SyntaxError
        ...

这是因为映射模式必须知道要查找哪个键。合法形式包括:

match data:
    case {"type": message_type}:
        ...

或者:

class Keys:
    TYPE = "type"

match data:
    case {Keys.TYPE: message_type}:
        ...

9. 类模式:isinstance、属性提取与 __match_args__

类模式用于同时检查对象类型并读取属性:

class User:
    def __init__(self, user_id, name):
        self.user_id = user_id
        self.name = name


def describe(value):
    match value:
        case User(user_id, name):
            return f"{user_id}: {name}"
        case _:
            return "not a user"

类模式大致执行以下步骤:

  1. 检查模式中的 User 是否是类型对象;
  2. 执行类似 isinstance(value, User) 的检查;
  3. 读取匹配所需的属性;
  4. 对属性值继续执行子模式匹配。(docs.python.org)

9.1 关键字类模式

使用关键字模式可以直接写出属性名称:

match value:
    case User(user_id=user_id, name=name):
        ...

这通常比位置模式更稳定,因为它不依赖属性排列顺序。

9.2 位置类模式与 __match_args__

case User(user_id, name):

并不是自动调用构造函数,也不是把对象当作元组拆包。它会读取:

User.__match_args__

然后把位置参数转换成关键字属性。假设:

User.__match_args__ == ("user_id", "name")

那么:

case User(user_id, name):

逻辑上近似于:

case User(user_id=user_id, name=name):

如果位置模式数量超过 __match_args__ 的长度,或者 __match_args__ 不是字符串元组,就会产生 TypeError。(docs.python.org)

可以手动定义:

class Point:
    __match_args__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y


match Point(1, 2):
    case Point(x, y):
        print(x, y)

这里的 xy 是模式中的新绑定名称。

9.3 dataclass 与类模式

dataclass 很适合配合类模式,因为它提供清晰的数据模型:

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int


def quadrant(point):
    match point:
        case Point(x, y) if x > 0 and y > 0:
            return "第一象限"
        case Point(x, y) if x < 0 and y > 0:
            return "第二象限"
        case Point(x, y) if x < 0 and y < 0:
            return "第三象限"
        case Point(x, y) if x > 0 and y < 0:
            return "第四象限"
        case Point(0, 0):
            return "原点"
        case Point():
            return "坐标轴上"
        case _:
            return "not a point"

对于这种位置匹配,字段声明顺序会影响位置参数含义。若模型后续频繁演进,关键字模式通常更明确:

match point:
    case Point(x=x, y=y) if x > 0 and y > 0:
        return "第一象限"

dataclassfrozen=True 只影响对象属性修改能力,不会让模式匹配变成“按值不可变比较”:

from dataclasses import dataclass


@dataclass(frozen=True)
class User:
    id: int
    name: str


user = User(1, "Ada")

match user:
    case User(id=user_id, name=name):
        print(user_id, name)

模式读取的是属性值;是否允许后续修改对象,是数据类自身的另一个语义问题。


10. 守卫:模式成功后的第二道条件

守卫是 case 模式后的 if 表达式:

match value:
    case int(number) if number > 0:
        print("positive integer")
    case int(number):
        print("non-positive integer")

守卫不是模式的一部分,而是模式成功后才执行的普通表达式。

逻辑顺序是:

尝试 int(number)
    ├── 失败:不执行守卫,尝试下一个 case
    └── 成功:number 已绑定
              ├── number > 0 为真:执行代码块
              └── number > 0 为假:尝试下一个 case

官方规则保证:模式成功后,守卫执行时,模式中的名称绑定已经完成。守卫按 case 顺序执行;守卫抛出的异常不会被 match 自动吞掉,而是向外传播。(docs.python.org)

10.1 守卫可以使用绑定变量

match response:
    case {"status": status, "body": body} if status == 200:
        return body
    case {"status": status} if status >= 400:
        raise RuntimeError(f"HTTP error: {status}")
    case _:
        raise ValueError("invalid response")

statusbody 来自模式,守卫负责表达不能仅靠结构描述的业务条件。

10.2 守卫不是异常处理机制

下面的代码不会把 KeyError 自动转成匹配失败:

def parse(value):
    match value:
        case {"items": items} if items[0] > 0:
            return items
        case _:
            return []

如果 items 是空列表,items[0] 会抛出 IndexError。守卫为表达式,表达式中的异常会正常传播。应显式处理结构:

def parse(value):
    match value:
        case {"items": [first, *rest]} if first > 0:
            return [first, *rest]
        case _:
            return []

11. as 模式:既检查结构,又保存整体对象

as 模式用于同时匹配子模式并绑定完整主题值:

match value:
    case {"type": "user", "id": user_id} as user:
        print(user_id)
        print(user)

成功后:

user_id = user["id"]
user = 整个字典对象

其逻辑是:

先匹配 {"type": "user", "id": user_id}
匹配成功后,再执行 user = subject

as 右侧必须是捕获名称,不能是 _。(docs.python.org)

它适合在既需要局部字段,又需要保留原始对象时使用:

match event:
    case {"type": "created", "id": event_id} as raw_event:
        audit(raw_event)
        return event_id

12. OR 模式:多个形状共享一个分支

OR 模式使用 |

match command:
    case "quit" | "exit" | "q":
        return "stop"
    case "help" | "?":
        return "help"
    case _:
        return "unknown"

它会依次尝试各个子模式,任意一个成功,整个 OR 模式就成功。

但 OR 模式有一个重要约束:

所有备选模式必须绑定相同的一组名称。

合法:

match value:
    case ("ok", result) | ("success", result):
        print(result)

非法:

match value:
    case ("ok", result) | ("error", message, code):
        ...

第一个分支绑定 result,第二个分支绑定 messagecode,成功后代码块无法确定哪些变量存在,因此会产生语法错误。官方规范要求 OR 模式的每个子模式绑定相同名称集合。(docs.python.org)


13. 捕获变量的作用域与泄漏

模式中的绑定不是代码块局部变量。它们通常会在 match 语句之后继续存在:

def f(value):
    match value:
        case {"id": user_id}:
            print(user_id)

    return user_id

当匹配成功时,user_id 可以在 match 之后使用。

但这也会造成两个问题。

13.1 没有匹配时,变量可能未定义

def f(value):
    match value:
        case {"id": user_id}:
            pass

    return user_id

当输入不包含 "id" 时,user_id 没有绑定,函数会抛出:

UnboundLocalError

应让每条控制路径都返回结果,或者提供兜底分支:

def f(value):
    match value:
        case {"id": user_id}:
            return user_id
        case _:
            return None

13.2 守卫失败后,名称可能已经绑定

def f(value):
    match value:
        case int(number) if number > 10:
            return "large"
        case _:
            return f"number={number}"

value5 时:

  1. int(number) 模式成功;
  2. number 绑定为 5
  3. 守卫 number > 10 失败;
  4. 继续尝试 _
  5. number 可能仍为 5

虽然这是语言语义允许的结果,但不应把守卫失败后的残留绑定当作设计接口。更清晰的写法是:

def f(value):
    match value:
        case int(number) if number > 10:
            return "large"
        case int(number):
            return f"number={number}"
        case _:
            return "not an integer"

13.3 失败匹配后的绑定不能依赖

def f(value):
    match value:
        case [x, 0]:
            pass
        case _:
            pass

    return x

当输入是 [1, 2] 时,第一项子模式可能已经绑定过 x,但整个模式失败。规范不保证此时 x 是否存在、是否保持原值或是否发生变化。(docs.python.org)

因此,模式绑定应只在对应成功分支中使用,不要把它当作一种“试探性赋值”。


14. 不可穷尽的模式与不可反驳分支

match 不要求必须覆盖所有可能值:

def f(value):
    match value:
        case 0:
            return "zero"

输入 1 时,函数自然执行到末尾并返回 None

如果需要穷尽处理,应显式写兜底分支:

def f(value):
    match value:
        case 0:
            return "zero"
        case _:
            return "other"

case _ 是不可反驳分支,因为它必定成功。捕获模式也不可反驳:

case value:
    ...

不可反驳分支最多一个,并且必须位于最后;否则后面的分支永远不可达。(docs.python.org)

下面的代码会产生语法错误或被静态检查工具报告为不可达:

match value:
    case _:
        return "anything"
    case 1:
        return "one"

带守卫的捕获模式不属于无条件不可反驳分支:

match value:
    case item if item is not None:
        return "non-null"
    case _:
        return "other"

因为守卫可能为假。


15. 模式优先级就是业务规则优先级

case 按书写顺序匹配,因此分支顺序会改变程序含义:

def classify(value):
    match value:
        case int(number):
            return "integer"
        case 1:
            return "one"
        case _:
            return "other"

classify(1) 返回 "integer",因为 int(number) 已经成功,case 1 根本不会执行。

应该把更具体的模式放在更宽泛的模式之前:

def classify(value):
    match value:
        case 1:
            return "one"
        case int(number):
            return "integer"
        case _:
            return "other"

可以把模式排序理解为集合包含关系:

P1P2P_1 \supseteq P_2

如果第一个模式覆盖的输入集合包含第二个模式,那么第二个模式必须先写,否则它会被遮蔽。

例如:

case {"type": kind}:
case {"type": "user"}:

第一个模式接受所有包含 "type" 的映射,因此第二个分支永远无法处理 "user"

正确顺序是:

case {"type": "user"}:
case {"type": kind}:

16. 一个完整的事件分派示例

下面使用 Enumdataclass、映射模式、类模式、OR 模式和守卫组成一个完整示例。

from dataclasses import dataclass
from enum import Enum


class EventType(Enum):
    USER_CREATED = "user_created"
    USER_DELETED = "user_deleted"


@dataclass(frozen=True)
class UserCreated:
    user_id: int
    name: str


@dataclass(frozen=True)
class UserDeleted:
    user_id: int


def handle(event):
    match event:
        case UserCreated(user_id=user_id, name=name) if user_id > 0:
            return f"create user {user_id}: {name}"

        case UserDeleted(user_id=user_id) if user_id > 0:
            return f"delete user {user_id}"

        case {"type": EventType.USER_CREATED.value, "user_id": user_id, "name": name}:
            return f"create serialized user {user_id}: {name}"

        case {"type": EventType.USER_DELETED.value, "user_id": user_id}:
            return f"delete serialized user {user_id}"

        case ("quit" | "exit",):
            return "stop"

        case _:
            raise ValueError(f"unsupported event: {event!r}")

测试:

events = [
    UserCreated(1, "Ada"),
    UserDeleted(2),
    {
        "type": "user_created",
        "user_id": 3,
        "name": "Grace",
    },
    ("quit",),
]

for event in events:
    print(handle(event))

输出:

create user 1: Ada
delete user 2
create serialized user 3: Grace
stop

每个分支承担不同职责:

  • UserCreated(...):处理内存中的领域对象;
  • UserDeleted(...):处理另一种领域对象;
  • 字典模式:处理序列化或外部输入;
  • 元组模式:处理简单命令;
  • _:拒绝未支持的输入。

这里没有把所有逻辑压缩到一个巨大模式中,而是让数据形状和业务条件分别表达:

case UserCreated(user_id=user_id, name=name) if user_id > 0:

类型和字段由模式处理,user_id > 0 由守卫处理。


17. 模式匹配与 if 的边界

match 适合描述输入的形状差异

match message:
    case {"type": "user", "id": user_id}:
        ...
    case {"type": "system", "level": level}:
        ...

if 更适合描述同一结构内部的普通条件:

match message:
    case {"type": "user", "id": user_id}:
        if user_id > 0:
            ...

也可以使用守卫:

match message:
    case {"type": "user", "id": user_id} if user_id > 0:
        ...

两者的区别在于,守卫适合一个简短、直接依赖绑定变量的条件;如果条件包含复杂副作用、异常处理或多步流程,普通代码通常更容易诊断。

例如,不要把一段长流程塞入守卫:

match request:
    case {"user_id": user_id} if validate_user(load_user(user_id)):
        ...

这段代码同时隐藏了:

  • load_user() 是否有副作用;
  • load_user() 是否可能抛异常;
  • validate_user() 是否昂贵;
  • 守卫失败后是否继续尝试其他分支。

可以拆开:

match request:
    case {"user_id": user_id}:
        user = load_user(user_id)
        if validate_user(user):
            return process(user)

return reject(request)

这样仍然使用模式匹配完成结构识别,但将可能失败的业务流程放到普通语句中。


18. 常见错误与诊断方法

错误一:把捕获模式当成常量

OK = "ok"

match result:
    case OK:
        ...

问题:OK 是捕获模式。

修复:

class Status:
    OK = "ok"

match result:
    case Status.OK:
        ...

或者:

match result:
    case "ok":
        ...

错误二:把映射模式当成完整字典比较

match data:
    case {"id": user_id}:
        ...

这不是要求字典只有一个 "id" 键,而是要求至少包含 "id"。如果必须拒绝额外字段,应显式检查:

match data:
    case {"id": user_id, **extra} if not extra:
        ...

错误三:把类模式当成构造函数调用

case User(user_id, name):

它不会创建 User 对象,也不会调用构造函数,而是检查主题对象是否为 User 实例,并读取属性。类模式的定位是类型和属性匹配。(docs.python.org)

错误四:忽略 __match_args__

@dataclass
class User:
    name: str
    user_id: int

此时:

case User(a, b):

位置绑定通常对应:

a = name
b = user_id

不要仅凭构造函数调用习惯猜测位置模式含义。需要稳定性时,使用关键字模式:

case User(name=name, user_id=user_id):

错误五:在兜底分支后继续写分支

match value:
    case _:
        return "default"
    case 1:
        return "one"

case _ 必定成功,后面的分支不可达。把具体模式放在前面。

错误六:依赖失败匹配后的变量

match value:
    case [x, y, 0]:
        ...
    case _:
        print(x)

如果第一个模式失败,不应假设 xy 没有绑定,也不应假设它们保持原值。失败匹配的中间绑定状态不是可靠接口。(docs.python.org)


19. 如何选择模式形式

同一个问题可以有多种写法。选择时,首先看输入的稳定边界。

外部 JSON 或字典输入

优先使用映射模式:

match payload:
    case {"kind": "created", "id": int(object_id)}:
        ...

它可以同时检查键存在、值类型和嵌套结构。

内部领域对象

优先使用类模式:

match command:
    case CreateUser(user_id=user_id, name=name):
        ...

它比对字典键名的依赖更少,且能利用类型模型表达领域结构。

固定位置协议

使用序列模式:

match message:
    case ["OK", request_id, *parts]:
        ...

但位置协议对顺序敏感,字段扩展可能破坏调用方。

常量集合

使用限定值模式或枚举:

match state:
    case State.RUNNING | State.PAUSED:
        ...

不要使用裸名称伪装常量。

复杂业务条件

让模式完成结构筛选,让守卫或普通 if 完成业务判断:

match order:
    case Order(total=total, status=status) if status is Status.PAID and total > 0:
        ...

如果守卫变成多次函数调用、异常处理或副作用流程,应拆出普通语句。


20. 最终应形成的心智模型

可以把每个 case 理解为以下四阶段:

主题值
  │
  ▼
模式检查 ──失败──> 下一个 case
  │
  ▼
名称绑定
  │
  ▼
守卫检查 ──失败──> 下一个 case
  │
  ▼
执行代码块

其中最容易出错的地方有四个:

  1. 裸名称是捕获,不是常量
  2. _ 只在模式位置是通配符
  3. 守卫在模式成功、绑定完成后才执行
  4. 失败匹配后的绑定状态不可依赖

掌握这些规则后,match 就不再只是语法糖,而是一种可以明确表达输入结构的控制流工具:字面量负责比较,序列负责位置,映射负责键,类模式负责类型和属性,捕获负责取值,as 负责保存整体,OR 模式负责合并形状,守卫负责附加条件,_ 负责明确兜底。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。