楔子
Python 从 3.10 版本开始,引入了 match case 语句,功能非常强大。如果你熟悉 Rust,你会发现和 Rust 里的 match 表达式非常相似。
我们先来学习一下相关语法,然后再剖析它的实现原理。
match case 基本语法
首先是最基本的等值匹配:
import random
value = random.choice([200, 302, 400, 404, 500])
match value:
case 200:
print("服务端正常返回响应")
case 302:
print("重定向")
case 400:
print("请求失败")
case 404:
print("资源不存在")
case 500:
print("服务端内部出现错误")
注:match 和 case 目前还不是关键字。
整个流程应该很好理解,并且每次只会执行 match 里的一个 case 分支。那么问题来了,如果我只关注里面的一个分支,其余情况都统一处理,该怎么做呢?
match value:
case 200:
print("服务端响应成功")
case _:
print("状态码不是 200,具体原因请排查")
和 Rust 一样,使用一个下划线表示默认分支。
value = 123
x = 456
match value:
case x:
print(f"case x 分支执行,x = {x}")
"""
case x 分支执行,x = 123
"""
print(x)
"""
123
"""
这个例子估计让人有些困惑,为啥 case x 可以匹配上,它不是等于 456 吗,和 value 不相等啊。其实 case 后面的 x 相当于一个占位符,它可以匹配任何值,并且匹配之后,相当于创建了一个全局变量 x。
match (1, 2):
case x:
print(f"case x 分支执行,x = {x}")
"""
case x 分支执行,x = (1, 2)
"""
print(x)
"""
(1, 2)
"""
match {"name": "古明地觉", "age": 17}:
case x:
print(f"case x 分支执行,x = {x}")
"""
case x 分支执行,x = {'name': '古明地觉', 'age': 17}
"""
print(x)
"""
{'name': '古明地觉', 'age': 17}
"""
所以 case x 相当于创建一个变量 x,显然任何值都可以和它匹配。
match (123, 456):
case (1, x):
print(f"case (1, x) 分支执行,x = {x}")
case (123, x):
print(f"case (123, x) 分支执行,x = {x}")
"""
case (123, x) 分支执行,x = 456
"""
match {"name": "古明地觉"}:
case {"name": x}:
print(f"x = {x}")
"""
x = 古明地觉
"""
看里面的 case (1, x),变量 x 可以代表任何值,所以它能匹配的是第一个元素为 1、第二个元素任意的二元组。同理 case (123, x) 能匹配的是第一个元素为 123、第二个元素任意的二元组。
match [1, 2, 3]:
case [1]:
print("case [1] 匹配成功")
case [x]:
print(f"case [x] 匹配成功,x = {x}")
case x:
print(f"case x 匹配成功,x = {x}")
"""
case x 匹配成功,x = [1, 2, 3]
"""
注意:case [x] 和 case x 存在本质的区别,我们解释一下这三个 case 分支。
case [1]:只能精确匹配 [1]。
case [x]:匹配长度为 1 的列表,但列表里的元素可以是任意值。
case x:由于 x 就是普通的变量,所以它可以匹配任意值。
如果我有好几个选项的处理逻辑相同,如何将它们写在同一个 case 分支中呢。
value = "3"
match value:
case 1 | 3 | 5 | 7 | 9 | "1" | "3" | "5" | "7" | "9":
print("10 以内的奇数")
case _:
print("不是 10 以内的奇数")
"""
10 以内的奇数
"""
如果有多个选项,那么使用 | 连接起来即可,并且使用 | 连接起来的必须都是常量。
然后每一个 case 后面还可以绑定卫语句,举个例子。
value = [123, 456, 789]
match value:
case [x, y, z] if z % 2 == 0:
print(f"case [x, y, z] 匹配成功,并且 z 为偶数")
case [x, y, z] if z % 2 == 1:
print(f"case [x, y, z] 匹配成功,并且 z 为奇数")
"""
case [x, y, z] 匹配成功,并且 z 为奇数
"""
print(x, y, z)
"""
123 456 789
"""
说白了卫语句就是对 case 的匹配范围做进一步限制,所以通过卫语句,我们可以实现 if 的功能。
"""
score = 85
if score >= 85:
print("Good")
elif score >= 60:
print("Normal")
else:
print("Bad")
"""
score = 85
match score:
case x if x >= 85:
print("Good")
case x if x >= 60:
print("Normal")
case _:
print("Bad")
"""
Good
"""
以上就是 Python 的 match case,非常简单,至于更多用法可以参考官网。
match case 字节码指令
我们举个简单的例子,看一下字节码长什么样子。
import dis
code_string = """
match v:
case 1:
print("v = 1")
case 2:
print("v = 2")
case _:
print("v = UnKnown")
"""
dis.dis(compile(code_string, "<file>", "exec"))
字节码指令如下:
0 RESUME 0
2 LOAD_NAME 0 (v)
# case 1:
4 COPY 1
6 LOAD_CONST 0 (1)
8 COMPARE_OP 40 (==)
12 POP_JUMP_IF_FALSE 10 (to 34)
14 POP_TOP
# print("v = 1")
16 PUSH_NULL
18 LOAD_NAME 1 (print)
20 LOAD_CONST 1 ('v = 1')
22 CALL 1
30 POP_TOP
32 RETURN_CONST 5 (None)
# case 2:
>> 34 LOAD_CONST 2 (2)
36 COMPARE_OP 40 (==)
40 POP_JUMP_IF_FALSE 9 (to 60)
# print("v = 2")
42 PUSH_NULL
44 LOAD_NAME 1 (print)
46 LOAD_CONST 3 ('v = 2')
48 CALL 1
56 POP_TOP
58 RETURN_CONST 5 (None)
# case _:
>> 60 NOP
# print("v = UnKnown")
62 PUSH_NULL
64 LOAD_NAME 1 (print)
66 LOAD_CONST 4 ('v = UnKnown')
68 CALL 1
76 POP_TOP
78 RETURN_CONST 5 (None)
整个字节码指令和 if 语句类似,都是先判断,如果条件不匹配,则通过 POP_JUMP_IF_FALSE 指令跳转到下一个分支。
再来看个例子:
match v:
case 1 | 2 | 3 | 4:
print("v in (1, 2, 3, 4)")
case _:
print("v = UnKnown")
它的字节码指令长什么样子,相信你也能猜出来。
0 RESUME 0
2 LOAD_NAME 0 (v)
# 判断 v 是否等于 1
4 COPY 1
6 LOAD_CONST 0 (1)
8 COMPARE_OP 40 (==)
# 如果 v == 1 为假,跳转到偏移量为 16 的指令
12 POP_JUMP_IF_FALSE 1 (to 16)
# 否则说明 v == 1 为真,跳转到偏移量为 56 的指令
14 JUMP_FORWARD 20 (to 56)
# 判断 v 是否等于 2
>> 16 COPY 1
18 LOAD_CONST 1 (2)
20 COMPARE_OP 40 (==)
# 如果 v == 2 为假,跳转到偏移量为 28 的指令
24 POP_JUMP_IF_FALSE 1 (to 28)
# 否则说明 v == 2 为真,跳转到偏移量为 56 的指令
26 JUMP_FORWARD 14 (to 56)
# 判断 v 是否等于 3
>> 28 COPY 1
30 LOAD_CONST 2 (3)
32 COMPARE_OP 40 (==)
# 如果 v == 3 为假,跳转到偏移量为 40 的指令
36 POP_JUMP_IF_FALSE 1 (to 40)
# 否则说明 v == 3 为真,跳转到偏移量为 56 的指令
38 JUMP_FORWARD 8 (to 56)
# 判断 v 是否等于 4
>> 40 COPY 1
42 LOAD_CONST 3 (4)
44 COMPARE_OP 40 (==)
# 如果 v == 4 为假,跳转到偏移量为 52 的指令
48 POP_JUMP_IF_FALSE 1 (to 52)
# 否则说明 v == 4 为真,跳转到偏移量为 56 的指令
50 JUMP_FORWARD 2 (to 56)
>> 52 POP_TOP
# 说明 v 和 1、2、3、4 都不相等
# 即 case 1 | 2 | 3 | 4 这个分支不成立
# 那么跳转到偏移量为 76 的指令,即 case _: 分支
54 JUMP_FORWARD 10 (to 76)
# 偏移量为 56 的指令,即 print("v in (1, 2, 3, 4)")
>> 56 POP_TOP
58 PUSH_NULL
60 LOAD_NAME 1 (print)
62 LOAD_CONST 4 ('v in (1, 2, 3, 4)')
64 CALL 1
72 POP_TOP
74 RETURN_CONST 6 (None)
# case _:
>> 76 NOP
78 PUSH_NULL
80 LOAD_NAME 1 (print)
82 LOAD_CONST 5 ('v = UnKnown')
84 CALL 1
92 POP_TOP
94 RETURN_CONST 6 (None)
没有任何难度,所谓 match case 其实本质上和 if 一样,都是顺序匹配加上跳转。
小结
以上我们就简单讨论了 match case,可以看到 Python 从 Rust 里面也借鉴了一些有趣的设计。不过 match 是 3.10 才引入到 Python 中的,使用的时候要注意可移植性。