几个月前接手了一个物联网平台的规则引擎模块,里面有一段解析用户自定义规则的代码,大概长这样:根据配置中的 `action` 字段的值,执行不同的处理,而且每种动作所携带的参数结构还不一样。为了处理这堆五花八门的参数,代码里塞满了 `if action == “send_email”` 然后 `if “recipient” in params` 再 `isinstance(params[“recipient”], list)`……一路下来嵌套了五六层。添加一个新动作类型需要改三个地方,漏掉任何一处都会在生产上炸得很难看。
那段时间正好想起 Python 3.10 引入的 `match case` 模式匹配,它的设计远比简单的 switch 强大——可以匹配数据类型、结构、甚至直接解构对象属性。于是花了两个下午把那套规则引擎的核心逻辑用模式匹配重写了一遍,结果不仅代码行数少了四成,新增动作类型也从改三处变成了只加一个匹配分支。这篇文章就以两个实际场景——JSON 配置解析与表达式求值——来还原这次重写的思路和具体用法。
一、从简单的值匹配到结构化解析
`match case` 最基础的用法和多数语言里的 switch 相似,匹配的是具体的值:
def handle_status(code):
match code:
case 200:
return "成功"
case 404:
return "未找到"
case 500:
return "服务器错误"
case _:
return "未知状态"
但真正让它变得不可或缺的是结构化匹配的能力。比如我们拿到的 JSON 配置对象经过 `json.load` 后是一个字典和列表的组合,可以直接在 `match` 里描述期望的结构:
def parse_action(config):
match config:
case {"type": "email", "recipient": str(addr), "subject": str(subj)}:
return f"发送邮件至 {addr},主题:{subj}"
case {"type": "sms", "phone": str(num), "content": str(text)}:
return f"发送短信至 {num},内容:{text}"
case {"type": "webhook", "url": str(url), "method": ("GET" | "POST") as method}:
return f"调用 {method} {url}"
case _:
raise ValueError(f"未知动作: {config}")
这里 `{“type”: “email”, “recipient”: str(addr), …}` 不仅仅是在匹配键值对,它还做了三件事:一是校验 `type` 字段的值必须是字符串 `”email”`;二是将 `recipient` 的值绑定到变量 `addr`,并同时要求其类型为 `str`;三是如果字典中缺少这些键或类型不符,匹配直接失败,落到下一个 `case`。这种把类型检查和变量绑定合并在一条语句的写法,比原来那一堆 `if isinstance` 清爽太多。
二、配合数据类:让匹配如虎添翼
对于更复杂的配置,我们将 JSON 先反序列化为数据类(dataclass)实例,然后直接在 `match` 中按类的属性匹配。假设规则引擎中有多种动作类,我们用继承来建模:
from dataclasses import dataclass
class Action:
pass
@dataclass
class EmailAction(Action):
recipient: str
subject: str
@dataclass
class SmsAction(Action):
phone: str
content: str
@dataclass
class WebhookAction(Action):
url: str
method: str
然后在规则处理函数中,用 `match` 根据动作类型分发并直接解构属性:
def execute(action: Action):
match action:
case EmailAction(recipient=r, subject=s):
send_email(r, s)
case SmsAction(phone=p, content=c):
send_sms(p, c)
case WebhookAction(url=u, method=m):
call_webhook(u, m)
case _:
raise NotImplementedError("不支持的动作类型")
注意这里 `case EmailAction(recipient=r, subject=s)` 触发了类属性的提取,它等价于先检查 `action` 是否是 `EmailAction` 实例,然后把 `action.recipient` 绑定到 `r`。如果未来增加一个新的 `PushAction`,只需要添加一个 `case PushAction(…)` 分支,而且因为有 `case _` 的兜底,未处理的类型会立即报错,避免遗留。
三、守卫条件:进一步约束匹配
有时仅靠结构还不够,需要附加条件。比如邮件动作里如果收件人是空字符串就不应该执行。可以在 `case` 后加上 `if` 守卫:
match action:
case EmailAction(recipient=r, subject=s) if r != "":
send_email(r, s)
case EmailAction(recipient=r, subject=s):
log.warning("收件人为空,跳过邮件发送")
case _:
...
守卫条件只在模式匹配成功后进行评估,判断为 `False` 时继续尝试下一个 `case`。这样可以非常精细地处理边界情况,不需要在分支代码块中再套一层 `if`。
四、实战案例二:一个迷你计算器的AST解析
再来看一个更有趣的例子:如何用模式匹配实现一个简单的表达式求值器。我们定义几种节点类型:
@dataclass
class Number:
value: float
@dataclass
class Add:
left: object
right: object
@dataclass
class Subtract:
left: object
right: object
@dataclass
class Multiply:
left: object
right: object
旧式写法会用递归 `isinstance` 加类型强转,而现在的 `match` 可以直接解构递归定义:
def evaluate(node):
match node:
case Number(value=v):
return v
case Add(left=l, right=r):
return evaluate(l) + evaluate(r)
case Subtract(left=l, right=r):
return evaluate(l) - evaluate(r)
case Multiply(left=l, right=r):
return evaluate(l) * evaluate(r)
case _:
raise TypeError(f"未知节点: {node}")
这个递归函数的清晰程度接近数学定义,新增运算符只需要多一个 `case` 行。与之前写的一堆 `elif isinstance(node, Add)` 相比,阅读和维护的难度降低了不止一个数量级。
五、模式匹配的几个实用技巧
1. 匹配列表和元组: `match` 能直接解构序列。例如处理可变参数命令 `[“move”, 10, 20]` 可以写成:
case ["move", int(x), int(y)]:
move(x, y)
2. 用 `|` 组合多个备选: 如前所述,`(“GET” | “POST”)` 可以匹配其中任意一个字符串。
3. 捕获所有剩余元素: 使用 `*remaining` 把列表中未匹配的剩余部分收集起来,如 `case [“log”, *messages]`。
4. 匹配映射: 可以匹配字典中的特定键,并允许任意额外键存在(使用 `**rest`),例如 `case {“type”: “alert”, **rest}` 会匹配任意包含 `”type”: “alert”` 的字典,其余键值放入 `rest` 字典中。
六、迁移过程中遇到的一个小坑
在重写规则引擎时,有一个地方一直匹配不到新的分支,排查后发现是数据类字段类型用了 `Optional[str]`,但实际运行时传入的可能是一个空字符串而不是 `None`。`match` 中的 `str(addr)` 只检查是否为 `str` 类型,不关心是否空串。这一点和以往用 `isinstance` 的行为一致,但因为之前总在业务代码里会跟进一层 `if not addr`,现在要记得将这种检查放到守卫条件中。换句话说,模式匹配不会自动补足业务校验,它只负责结构和类型,附加的逻辑还得自己写守卫。
七、总结
把规则引擎的核心解析器用 `match case` 改完之后,一个明显的变化是新增动作类型的时间从十几分钟降到了两分钟——只需要在动作定义文件里加一个数据类,然后在匹配函数里加一个 `case` 分支,再无其他。更重要的是,代码的意图变得非常直白:你几乎可以把匹配语句当成一个配置表来读,而不是去猜层层条件分支里到底发生了什么。
如果你的项目里还有那种难以维护的 `if-elif` 长链,或者用了一大堆 `isinstance` 来做类型分发,强烈建议抽出一个模块用 `match case` 试着重写。这个特性并不是花哨的语法糖,而是实实在在能降低认知负担的利器。

