match-case 语句
match-case 是 Python 3.10 引入的结构模式匹配语句。它表面上像 C、Java 中的 switch,但功能更接近 Rust 或 Haskell 的模式匹配——不仅能比较值,还能解构数据结构、绑定变量,并根据值的"形状"选择执行路径。
基本语法
match 接受一个表达式(称为主语),将其值与一系列 case 模式依次比较,只有第一个匹配的模式会被执行:
def http_error(status):
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong with the internet"
print(http_error(404)) # Not found
最后一个 case _ 中的 _ 是通配符,它匹配任何值,相当于 switch 中的 default。如果没有通配符且所有 case 都不匹配,程序直接跳过整个 match 语句,不执行任何分支。
字面值模式与或模式
最简单的模式是字面值模式,将主语与常量直接比较:
dept = "技术部"
match dept:
case "技术部":
print("负责产品研发")
case "产品部":
print("负责需求设计")
case "运营部":
print("负责用户增长")
case _:
print("未知部门")
可以用 | 在一个 case 中组合多个字面值,实现"或"逻辑:
match status:
case 401 | 403 | 404:
return "Not allowed"
这相当于 if status in (401, 403, 404),但写法更紧凑。
捕获模式
模式可以包含变量,将主语中的值绑定到变量名。这种能力让 match 远超简单的等值比较:
point = (0, 5)
match point:
case (0, 0):
print("原点")
case (0, y):
print(f"Y 轴上,Y={y}")
case (x, 0):
print(f"X 轴上,X={x}")
case (x, y):
print(f"X={x}, Y={y}")
case _:
raise ValueError("不是有效的点")
当 point 为 (0, 5) 时,(0, y) 模式匹配成功,变量 y 被绑定为 5。(x, y) 虽然也能匹配,但由于 case 按顺序执行且只匹配第一个,不会到达该分支。
捕获模式与解包赋值 (x, y) = point 概念类似,但增加了字面值约束——case (0, y) 要求第一个元素必须是 0。
类模式与属性捕获
如果数据用类组织,可以用"类名 + 参数列表"的形式匹配并提取属性:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(0, 5)
match p:
case Point(x=0, y=0):
print("原点")
case Point(x=0, y=y):
print(f"Y 轴上,Y={y}")
case Point(x=x, y=0):
print(f"X 轴上,X={x}")
case Point():
print("平面上某处")
case _:
print("不是 Point")
Point() 匹配任何 Point 实例,无论属性值如何。通过设置类的 __match_args__ 属性,还可以使用位置参数:
class Point:
__match_args__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
match p:
case Point(0, 0):
print("原点")
case Point(0, y):
print(f"Y={y}")
此时 Point(0, y)、Point(0, y=y)、Point(x=0, y=y) 是等价的。
序列模式与扩展解包
序列模式可以匹配列表、元组等序列,并支持扩展解包:
match points:
case []:
print("没有点")
case [Point(0, 0)]:
print("只有一个原点")
case [Point(x, y)]:
print(f"单点 ({x}, {y})")
case [Point(0, y1), Point(0, y2)]:
print(f"Y 轴上两点:{y1}, {y2}")
case [first, *rest]:
print(f"第一个:{first},其余:{rest}")
case _:
print("其他情况")
*rest 捕获剩余元素,类似于解包赋值。如果不需要绑定剩余元素,可以用 *_:
case [first, *_]:
print(f"至少有一个元素,第一个是 {first}")
注意序列模式不能匹配迭代器或字符串,只能匹配序列类型(列表、元组等)。
映射模式
映射模式用于匹配字典,捕获指定键的值:
config = {"bandwidth": 100, "latency": 20, "region": "ap-east"}
match config:
case {"bandwidth": b, "latency": l}:
print(f"带宽:{b},延迟:{l}")
case {"bandwidth": b}:
print(f"仅带宽:{b}")
case {}:
print("空配置")
case _:
print("非字典")
映射模式遵循"子集匹配"原则:主语字典可以包含额外的键,只要指定的键存在即可。这与序列模式的"精确匹配"不同。**rest 解包也受支持,但 **_ 是冗余的,因此不允许使用。
守卫子句
向模式添加 if 条件,可以进一步约束匹配结果,这种 if 子句称为守卫子句(guard):
match point:
case Point(x, y) if x == y:
print(f"对角线上,坐标 {x}")
case Point(x, y):
print(f"不在对角线上")
守卫子句在模式匹配成功后求值,如果为假,则 match 继续尝试下一个 case。注意值的捕获发生在守卫求值之前,因此守卫中可以使用已绑定的变量。
与 if-elif 的对比
match-case 和 if-elif 都能处理多分支,但适用场景不同:
if-elif:条件可以是任意布尔表达式,适合复杂逻辑判断、范围比较match-case:专注于值的"形状"和结构,适合等值比较、数据解构、类型分发
# if-elif 更适合范围判断
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
# match-case 更适合等值和结构判断
match status:
case 200 | 201:
return "成功"
case 404:
return "未找到"
官方文档指出:"如果是把一个值与多个常量进行比较,或者检查特定类型或属性,match 语句更有用。"
具名常量
模式可以使用具名常量(如枚举成员),但必须以带点号的名称出现,防止被解释为捕获变量:
from enum import Enum
class Color(Enum):
RED = 'red'
GREEN = 'green'
BLUE = 'blue'
color = Color.RED
match color:
case Color.RED:
print("红色")
case Color.GREEN:
print("绿色")
case Color.BLUE:
print("蓝色")
如果写成 case RED:,RED 会被当作新变量绑定,而非引用 Color.RED。
常见错误
忘记通配符导致静默跳过:
match x:
case 1:
print("一")
case 2:
print("二")
# x=3 时什么都不发生,没有报错
混淆捕获变量与常量:
RED = 1
match x:
case RED: # 错误:RED 被视为新变量,总是匹配
print("红色")
常量应通过模块/类限定(如 Status.RED)来避免被捕获。
match-case 为 Python 带来了声明式的数据匹配能力,在处理复杂数据结构、状态机、AST 遍历等场景时,代码比等价的 if-elif 链更清晰、更不易出错。