Python 3.10 新特性 —— 结构化模式匹配(Structural Pattern Match)详解

switch-case

众所周知,Python 中是没有类似 switch-case 结构的语法的。但是自从 3.10 版本发布以后,这种说法就已经成为历史了。

Java 中的 switch 语句类似如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
var option = 3;

switch (option) {
case 1:
System.out.println("You have chosen option 1.");
break;
case 2:
System.out.println("You have chosen option 2.");
break;
case 3:
System.out.println("You have chosen option 3.");
break;
default:
System.out.println("Sorry you chose an invalid option.");
break;
}
}
}

看起来就像是另一种形式的 if-else 语句。以某个变量值作为判断条件,根据不同的判断结果执行对应的语句,最终形成一种流程上的分支结构。
这也许是 Python 不去实现它的依据(借口)之一?都已经有了 if-else 可以足够轻松地完成同样的事情。

There should be one– and preferably only one –obvious way to do it.

字典映射

在模式匹配出现之前,对于分支相当多的判断语句,Python 建议通过字典映射(dictionary mapping)来实现。

1
2
3
4
5
6
7
8
def function_map(option):
return {
1: lambda : print('You have chose option 1.'),
2: lambda : print('You have chose option 2.'),
3: lambda : print('You have chose option 3.')
}.get(option, lambda: print('Sorry you chose an invalid option.'))

function_map(3)()

借助字典这种数据结构,以匹配条件作为键值,一一对应匹配后需要执行的命令。将 switch 结构中的条件判断转化为对字典键值的搜索匹配。

Pattern Match

用模式匹配实现 switch-case 语法,从形式上看就直观了很多:

1
2
3
4
5
6
7
8
9
10
11
option = 3

match option:
case 1:
print("You have chosen option 1.")
case 2:
print("You have chosen option 2.")
case 3:
print("You have chosen option 3.")
case _:
print("You chose an invalid option.")

实际上模式匹配不只有创建流程上的分支结构这一种功能,它的作用可以比单纯的 switch-case 语法强大的多。

模式匹配可以算是一种历史悠久的编程技巧了,经常可以在函数式编程语言中见到。比较有代表性的语言比如 Haskell。相对年轻的语言比如 Rust 也引入了功能强大的模式匹配语法。
模式匹配其实可以拆成两部分来理解:匹配和模式。
匹配部分可以发挥类似于 if-elseswitch 等条件判断语句的作用,生成一种分支结构;模式则定义了特定的规则即匹配的具体条件。更进一步的,还会对匹配到的对象进行解构(destructuring)或者说拆包(unpacking)。

以不同于模式匹配的正则表达式来说:

1
2
3
4
5
6
7
8
import re

source_str = 'cats are cute'
pattern = re.compile('(.*) are (.*)')

matched = re.match(pattern, source_str)
print(matched.groups())
# => ('cats', 'cute')

正则表达式规则中的 (.*) 分别匹配到源字符串中的 catscute,与此同时,还把这两个匹配项提取了出来。

而模式匹配相对来说,则不仅仅能够匹配和提取 catscute 等字符串类型,还能够匹配更复杂类型的对象,同时对匹配到的对象进行拆包操作。

比如下面的代码就对类型为元组的对象进行了匹配和拆包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def match_person(person):
match person:
case (name, 'M', age):
print(f'He is {name}, aged {age}.')
case (name, 'F', age):
print(f'She is {name}, aged {age}.')
case (name,):
print(f'We only know the name is {name}, others are secrets.')

person_A = ('John', 'M', 20)
person_B = ('Jenny', 'F', 18)
person_C = ('Lily',)

match_person(person_A)
# => He is John, aged 20.
match_person(person_B)
# => She is Jenny, aged 18.
match_person(person_C)
# => We only know the name is Lily, others are secrets.

match 关键字后面被匹配的对象,支持很多种复杂的类型。对应的 case 关键字后面的模式也同样灵活:

  • 列表或元组,如 (name, 18)
  • 字典,如 {"name": name, "age": 18}
  • 使用 * 匹配列表中的剩余部分,如 [first, *rest]
  • 使用 ** 匹配字典中的剩余部分
  • 匹配对象和对象的属性
  • 在模式中可以使用 | 逻辑或操作

模式匹配应用实例

创建一个 Python 程序,模拟交互式命令行的行为。

匹配字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def run_command(command: str) -> None:
match command:
case "quit":
print("Quitting the program.")
quit()
case "reset":
print("Resetting the system.")
case other:
print(f"Unknown command: {other!r}.")

def main() -> None:
while True:
command = input("$ ")
run_command(command)


if __name__ == '__main__':
main()

运行效果如下:

1
2
3
4
5
6
$ reset
Resetting the system.
$ abcdefg
Unknown command: 'abcdefg'.
$ quit
Quitting the program.

匹配列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def run_command(command: str):
match command.split():
case ["load", filename]:
print(f"Loading file: {filename}.")
case ["save", filename]:
print(f"Saving to file: {filename}.")
case ["quit" | "exit" | "bye"]:
print("Quitting the program.")
quit()
case _:
print(f"Unkown command: {command!r}.")


def main() -> None:
while True:
command = input("$ ")
run_command(command)


if __name__ == '__main__':
main()

运行效果:

1
2
3
4
5
6
7
8
$ load input_data.txt
Loading file: input_data.txt.
$ save output_data.txt
Saving to file: output_data.txt.
$ load input_data.txt output_data.txt
Unkown command: 'load input_data.txt output_data.txt'.
$ bye
Quitting the program.

匹配对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from dataclasses import dataclass
from typing import List
import shlex


@dataclass
class Command:
command: str
arguments: List[str]


def run_command(command: Command):
match command:
case Command(command="load", arguments=[filename]):
print(f"Loading file: {filename}.")
case Command(command="save", arguments=[filename]):
print(f"Saving to file: {filename}.")
case Command(command="quit" | "exit" | "bye", arguments=["--force" | "-f"]):
print("Sending SIGTERM and quitting the program.")
quit()
case Command(command="quit" | "exit" | "bye"):
print("Quitting the program.")
quit()
case _:
print(f"Unknown command: {command!r}.")


def main() -> None:
while True:
command, *arguments = shlex.split(input("$ "))
run_command(Command(command, arguments))


if __name__ == '__main__':
main()

运行效果:

1
2
3
4
5
6
7
8
$ not a command
Unknown command: Command(command='not', arguments=['a', 'command']).
$ load input_data.txt
Loading file: input_data.txt.
$ save output_data.txt
Saving to file: output_data.txt.
$ exit -f
Sending SIGTERM and quitting the program.

需要注意的是,模式匹配中各条 case 语句之间的前后顺序是至关重要的。通常来说,更“具体”更“精确”一些的规则要放在相对靠前的位置。
假如 case Command(command="quit" | "exit" | "bye") 为规则 1,Command(command="quit" | "exit" | "bye", arguments=["--force" | "-f"]) 为规则 2。
则更具体一些的规则 2 要放在规则 1 前面。因为模式匹配是从上到下依次检查每一个 case 语句,若遇到匹配的模式,则执行对应的命令。不再继续向下匹配。
由于严格符合规则 2 的对象一定也符合规则 1,当规则 1 位于规则 2 前面时,规则 2 永远也没有被匹配的机会。

可以想象成一种逐渐“滑落”的过程。比如写一个计算成绩等级的函数,可以这样实现:

1
2
3
4
5
6
7
def grade(score):
if score >= 90:
return 'A'
elif score >= 70:
return 'B'
elif score >= 60:
return 'C'

如果上面 if-else 的条件反着排,那就,所有人都是 C 了。。。

参考资料

A Closer Look At Structural Pattern Matching // New In Python 3.10!