序列模式匹配
match/case(Python 3.10 新增)表面上像 C 的 switch,但那只是一半。
真正的区别是解构(destructuring):它把数据结构拆开,按形状匹配,
顺手把值绑到变量上,还能做类型检查。
一句话概括:switch 只比相等,match 比的是形状。
一个命令路由器
Section titled “一个命令路由器”假设设备收到的命令是「词和数字组成的序列」,比如 ['BEEPER', 440, 3]:
match message: case ['BEEPER', frequency, times]: self.beep(times, frequency) case ['NECK', angle]: self.rotate_neck(angle) case ['LED', ident, intensity]: self.leds[ident].set_brightness(ident, intensity) case ['LED', ident, red, green, blue]: self.leds[ident].set_color(ident, red, green, blue) case _: raise InvalidCommand(message)逐条看:
- 第 1 条匹配「3 项的序列,第一项必须是字符串
'BEEPER'」。后两项随便是什么, 分别绑到frequency和times。 - 第 3 和第 4 条都以
'LED'开头,但项数不同(3 项 vs 5 项),所以不会互相吃掉。 - 最后一条是兜底。
_匹配任何东西,但不绑定。
一个序列模式匹配成功,需要同时满足三条:
- 被匹配的对象是序列
- 项数相同
- 每一项都能匹配,包括嵌套的项
[name, _, _, (lat, lon)] 匹配「4 项,最后一项是 2 项序列」。
>>> match ['Shanghai', 'CN', 24.9, (31.1, 121.3)]:... case [name, _, _, (lat, lon)]:... ...| 变量 | 值 |
|---|---|
name |
'Shanghai' |
lat |
31.1 |
lon |
121.3 |
方括号和圆括号在这里没区别
Section titled “方括号和圆括号在这里没区别”写 [a, b] 还是 (a, b) 完全一样。混着嵌套也行。
上面我写成「外层方括号、内层圆括号」,纯粹是为了少打几层括号。
三种不是序列的类型
Section titled “三种不是序列的类型”标准库里能直接做序列模式匹配的类型:
list memoryview array.arraytuple range collections.deque另一个限制:模式不解构非序列的可迭代对象。迭代器不行,range 和 deque 可以,
因为它们是序列。
在模式里做类型检查
Section titled “在模式里做类型检查”这是我觉得最有意思的一点。看这个模式:
case [str(name), _, _, (float(lat), float(lon))]:str(name) 和 float(lat) 长得像构造器调用,但在模式里语义完全不同 ——
它们是运行时类型检查。这个模式要求:
- 第 0 项必须是
str,并绑到name - 第 3 项必须是一对
float,分别绑到lat和lon
_ 和 as 和 *_
Section titled “_ 和 as 和 *_”# _ 匹配任意一项,但不绑定case [name, _, _, (lat, lon)]:
# as 把匹配到的那一段绑个名字case [name, _, _, (lat, lon) as coord]: # coord 是整对坐标
# *_ 匹配任意多项,不绑定case [str(name), *_, (float(lat), float(lon))]:
# *extra 匹配任意多项,绑成列表case [str(name), *extra, (float(lat), float(lon))]:_ 是唯一能在同一个模式里出现多次的变量。
守卫(guard)
Section titled “守卫(guard)”if 开头的守卫只在模式匹配成功后才会求值,而且能引用模式里绑定的变量:
case [name, _, _, (lat, lon)] if lon <= 0: print(...)这一条把「结构对」和「值合法」分成两步写,比在一个长表达式里混着写清楚得多。
一个更实在的例子
Section titled “一个更实在的例子”书里用 Peter Norvig 的 lis.py(132 行的 Scheme 解释器)演示重构。我写了自己版本 —— 一个逆波兰计算器:
match token: case float() | int() as number: stack.append(float(number)) case [op, *operands] if op in {"+", "-", "*", "/"}: ... case "+": b, a = stack.pop(), stack.pop() stack.append(a + b) ...[3, 4, '+'] 算出 7。嵌套也行:[["+", 1, 2, 3, 4], 2, "*"] 算出 20。
书里对这里有个说法我很认同:模式匹配是声明式编程。代码描述的是「要匹配什么」, 不是「怎么匹配」。代码的形状跟着数据的形状走。
| Scheme 语法 | 序列模式 |
|---|---|
(quote exp) |
['quote', exp] |
(if test conseq alt) |
['if', test, conseq, alt] |
(lambda (parms…) body…) |
['lambda', [*parms], *body] if body |
(define name exp) |
['define', Symbol() as name, exp] |
case _: raise SyntaxError(...)没有兜底的话,被匹配对象不匹配任何分支时,整个 match 语句什么都不做 ——
静默失败。这比抛异常难查得多。
test_match_is_not_switch_it_destructures—— 四条命令都命中正确的分支test_pattern_type_check_rejects_bad_data—— 坐标是int而不是float时不匹配test_star_wildcard_in_pattern—— 第一项类型不对时不匹配test_unmatched_command_raises—— 兜底分支抛异常