最近在对接一个第三方物流平台时,他们的API返回体堪称“变形怪”:成功时包裹在data字段里是一个对象;失败时data变成了字符串;如果部分成功,data又是一个列表,而且每个元素的结构还不一样。一开始我习惯性地用if-elif加isinstance来判断,写了快二十行还觉得逻辑绕来绕去。直到同事提了一句:“你这个用match-case会清楚很多。”重构之后,不仅代码量减了一半,分支意图也一下子明朗了。
Python 3.10引入的结构化模式匹配(match-case)一直在演进,到3.12时功能已经相当成熟。它不是switch的简单替代品,而是一套能解构对象、匹配类型、捕获值的声明式语法。这篇文章就顺着那个对接场景,把match-case的核心用法和实战技巧梳理出来,下次再遇到复杂条件分支,你手里的工具就不止if-elif了。
一个典型的“变形”响应
先看看那个物流API返回的几个样子。成功时:
{
"code": 0,
"msg": "ok",
"data": {
"tracking_no": "SF123456",
"status": "in_transit",
"details": ["已揽件", "运输中"]
}
}
失败时:
{
"code": -1,
"msg": "参数错误",
"data": "缺少运单号"
}
部分成功时(批量查询):
{
"code": 2,
"msg": "部分查询成功",
"data": [
{"tracking_no": "SF123456", "status": "delivered"},
"FAILED: INVALID_NO",
{"tracking_no": "SF789012", "status": "pending"}
]
}
如果针对每种情况写if 'data' in resp然后判断type(resp['data']),分支很容易膨胀。用模式匹配来处理,逻辑会变成精准的形状描述。
基础模式:匹配字面量和变量
先把响应解析成字典resp,然后用match resp来拆解:
def handle_response(resp):
match resp:
case {"code": 0, "msg": msg, "data": data}:
print(f"成功: {msg}, 数据为 {data}")
case {"code": -1, "msg": msg, "data": data}:
print(f"失败: {msg}, 原因: {data}")
case _:
print("未知响应格式")
这里的{"code": 0, ...}是一个映射模式,它要求resp是一个字典,并且code键必须严格等于0。同时我们捕获了msg和data的值作为变量,后续可以直接使用。这比写if resp.get('code') == 0:再一个个取键要直观得多。
使用守卫条件细化匹配
真实场景中,成功和失败可能共享一些结构,但需要通过更复杂的条件来区分。比如部分成功时code可能是正数。我们可以在模式后面加if守卫:
match resp:
case {"code": 0, "data": dict()}:
print("完全成功")
case {"code": int(n), "data": list() as data} if n > 0:
print(f"部分成功,共{len(data)}条结果")
case {"code": int(n), "data": str() as err} if n < 0:
print(f"错误码{n}: {err}")
注意dict()、list()这些是类模式,用来检查值的类型。守卫条件if n > 0让我们可以在模式匹配的基础上叠加数值判断,而且变量n来自前面的捕获,这和if嵌套有本质区别:模式本身已经完成了结构校验。
解构嵌套数据:序列模式与通配符
部分成功返回的data是一个混合列表,里面可能是字典也可能是字符串。我们需要分别处理每个元素:
case {"code": 2, "data": list(items)}:
for item in items:
match item:
case {"tracking_no": str(tno), "status": str(st)}:
print(f"单号 {tno} 状态为 {st}")
case str(err_msg):
print(f"查询失败: {err_msg}")
case _:
print("未知条目")
这里list(items)将data绑定到变量items,同时声明它必须是列表类型。内部的match item又分别处理字典和字符串。如果某个字段我们不需要,可以用_通配符忽略,比如{"tracking_no": _, "status": st}只提取状态。
用类模式匹配自定义对象
如果你把API返回封装成了数据类,匹配起来会更舒服:
from dataclasses import dataclass
@dataclass
class QueryResult:
code: int
msg: str
data: object
def process(result: QueryResult):
match result:
case QueryResult(code=0, msg=m, data=dict() as d):
return {"message": m, "tracking": d}
case QueryResult(code=n, msg=m, data=str() as e) if n < 0:
return {"error": e, "code": n}
case _:
raise ValueError("无法识别的结果")
类模式通过构造函数参数的形式匹配属性,不需要实现__match_args__也能用在数据类上。这种写法让业务逻辑的“形状”非常突出,一眼就能看出函数处理了哪几种情况。
完整实战:清洗物流查询结果
把上述知识整合起来,写一个清洗函数,将混乱的响应转化成统一的结构列表:
def normalize_response(resp: dict) -> list[dict]:
"""将各种格式的物流响应统一为 [{"tracking": ..., "status": ...}]"""
match resp:
case {"code": 0, "data": {"tracking_no": str(tno), "status": str(st)}}:
return [{"tracking": tno, "status": st}]
case {"code": int(n), "data": list(items)} if n >= 0:
results = []
for item in items:
match item:
case {"tracking_no": str(tno), "status": str(st)}:
results.append({"tracking": tno, "status": st})
case str(err):
results.append({"tracking": "unknown", "status": err})
case _:
results.append({"tracking": "unknown", "status": "格式错误"})
return results
case {"code": int(n), "data": str(err)} if n < 0:
# 整体错误,返回一个占位结果
return [{"tracking": "error", "status": err}]
case _:
raise ValueError(f"未预期的响应格式: {resp}")
# 测试三个样本
resp_ok = {"code": 0, "msg": "ok", "data": {"tracking_no": "SF123", "status": "delivered"}}
resp_fail = {"code": -1, "msg": "参数错误", "data": "缺少运单号"}
resp_partial = {
"code": 2, "msg": "部分成功",
"data": [
{"tracking_no": "A001", "status": "in_transit"},
"FAILED: B002",
{"tracking_no": "C003", "status": "pending"}
]
}
print(normalize_response(resp_ok))
print(normalize_response(resp_fail))
print(normalize_response(resp_partial))
输出结果:
[{'tracking': 'SF123', 'status': 'delivered'}]
[{'tracking': 'error', 'status': '缺少运单号'}]
[{'tracking': 'A001', 'status': 'in_transit'}, {'tracking': 'unknown', 'status': 'FAILED: B002'}, {'tracking': 'C003', 'status': 'pending'}]
整个函数的每个分支都精确描述了期望的数据形状,不需要任何isinstance调用,也不用担心键不存在抛出KeyError。因为模式匹配是整体性的,结构对不上会直接跳过该分支。
常见陷阱与实用建议
- 模式顺序很重要:
match从上到下匹配,第一个成功的分支会被执行。例如把case _:放在最前面,后面的分支永远不会执行。通常更具体的模式放前面,更宽泛的放后面。 - 不要过度使用匹配:如果一个简单的
if-else就能清晰表达,强行套用match反而增加认知负担。模式匹配的发力点是深度解构和类型检测同时存在的场景。 - 变量重复绑定:在同一个
case语句中,一个变量名只能被绑定一次。比如case {"a": x, "b": x}会报错,因为它试图将两个不同的值赋给同一个变量。 - 结合类型提示:给函数加上类型注解(如
dict或数据类),配合match可以让IDE提供更好的智能提示,尤其是在类模式中访问属性时。
总结
match-case不是语法糖,它是一种思维方式的转变:从“先取出来再判断”变成“描述你能接受的全部形状,并分别处理”。在处理外部数据——无论是API、CSV还是复杂JSON——时,这种方式能大大减少防御性编程的散乱代码。以前我们在Python里处理多形态数据要么用if-elif搭积木,要么引入第三方库做模式验证,现在原生语法就提供了优雅的解决方案。
下次当你面对一个结构多变、需要频繁判断类型的响应体时,不妨用match重写一遍。那种“我终于把混乱关进笼子里”的畅快感,会让你重新爱上写数据处理代码。

