流程控制语句 match case 是怎么实现的?

文摘   2024-11-04 10:14   北京  

楔子


Python 从 3.10 版本开始,引入了 match case 语句,功能非常强大。如果你熟悉 Rust,你会发现和 Rust 里的 match 表达式非常相似。

我们先来学习一下相关语法,然后再剖析它的实现原理。



match case 基本语法


首先是最基本的等值匹配:

import random

value = random.choice([200302400404500])

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 (12):
    
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 (123456):
    
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 [123]:
    
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 = [123456789]

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 中的,使用的时候要注意可移植性。

古明地觉的编程教室
Python、Rust 程序猿,你感兴趣的内容我都会写,点个关注吧(#^.^#)
 最新文章