该特性已经有 final 版本 since Python 3.10,出自 PEP 636,因此本文就该版本完整介绍 match 语句的各种花里胡哨的用法。

match 语句,或者说是 match-case 语句更为适合,和其他语言的 switch-case 语句类似,用作多条件的分支选择。在 Python 中,case 关键词的后面叫做模式(pattern)。

匹配字面值

这是最基本的用法,和:

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"

这是一个根据状态码返回响应消息的函数,不同值对应不同的结果,特别地,_ 是通配符,表示一定会匹配成功,必须在最后一个 case 分支,充当类似其他语言 default 分支的功能,但是位置没有 default 自由。

大多数字面值是按相等性比较的,但是单例对象 True, FalseNone 则是按标识号比较的。

使用 | 在一个模式中还可以组合多个字面值:

case 401 | 403 | 404:return "Not allowed"

| 不止用于值的组合,后续介绍的更为复杂模式都可以通过该符号表“或”关系,详见后面示例。

匹配常量

模式可以使用命名常量。 这些命名常量必须为带点号的名称以防止它们被解读为捕获变量,说白了就是枚举
这个没什么说的,直接上实例,后续结合其他模式再给出复杂示例。

from enum import Enumclass Color(Enum):RED = 'red'GREEN = 'green'BLUE = 'blue'color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))match color:case Color.RED:print("I see red!")case Color.GREEN:print("Grass is green")case Color.BLUE:print("I'm feeling the blues :(")

匹配序列

常见的序列如列表、集合、元组等,这里以列表为例,给出一个比较综合的示例:

def match_sequence(seq):match seq:case []:print("序列为空")case [a]:print(f"序列只有一个元素:{a}")case [x, y]:print(f"序列有两个元素,[{x}, {y}]")case [b, 1]:print(f"序列有两个元素,第一个元素为 {b}")case [_, _, c]:print(f"序列有三个元素,第三个元素为 {c}")case _:print(f"序列未知")match_sequence([])# 序列为空match_sequence([2]) # 序列只有一个元素:2match_sequence([4, 1])# 序列有两个元素,[4, 1]match_sequence([1, 2, 3]) # 序列有三个元素,第三个元素为 3match_sequence([1, 2, 3, 4, 5]) # 序列未知
  • 匹配序列时,会按照元素顺序逐一比较
  • 模式中可以使用变量进行解构赋值
  • 会按照从上到下的顺序匹配模式,匹配成功就执行子句返回结果
  • 序列中可以使用 _ 职位占位符,表示那里有元素,但不关注值,注意与模式 _ 含义区分
  • 不能匹配迭代器和字符串

扩展解构

序列模式支持扩展解构操作:[x, y, *rest](x, y, *rest) 的作用类似于解包赋值。 在 * 之后的名称也可以为 _,因此,(x, y, *_) 可以匹配包含至少两个条目的序列,而不必绑定其余的条目。

def match_sequence2(seq):match seq:case [1, *p]:print(p)case [3, a, *_]:print(f"a={a}")case [_, _, *q]:print(q)match_sequence2([1, 2, 3, 4])# [2, 3, 4]match_sequence2([3, 4, 5, 6])# a=4match_sequence2([2, 3, 4, 5])# [4, 5]

匹配字典

有了前面的基础,匹配字典值相对容易理解:

def match_dict(d):match d:case {"name": name, "age": age}:print(f"name={name},age={age}")case {"key": _, "value": value}:print(f"value={value}")case {"first": _, **rest}:print(rest)case _:passd1 = {"name": "ice", "age": 18}d2 = {"key": "k", "value": "v"}d3 = {"first": "one", "second": "two", "third": "three"}match_dict(d1)# name=ice,age=18match_dict()# value=vmatch_dict(d3)# {'second': 'two', 'third': 'three'}

**rest 等解构操作也支持,但 **_ 是冗余的,不允许使用。

匹配对象

通过类对象可以结构化你的数据,通过使用类名字后面跟一个类似构造函数的参数列表,这种模式可以将类的属性捕捉到变量中:

class Point:x: inty: intdef location(point):match point:case Point(x=0, y=0):print("坐标原点")case Point(x=0, y=y):print(f"Y={y}")case Point(x=x, y=0):print(f"X={x}")case Point(x=m, y=n):print(f"X={m}, Y={n}")case Point():print("这个点不在轴上")case _:raise ValueError("未法的坐标数据")p1 = Point()p2 = Point()p2.x = 0p2.y = 4p3 = Point()p3.x = 5p3.y = 6location(p1)# 这个点不在轴上location(p2)# Y=4location(p3)# X=5, Y=6
  • 这里特地将属性定义到类中而不是 __init__ 方法中,就是为了区分这种模式和构造函数的区别
  • 不写参数说明只关注是不是 Point 对象,其属性值无所谓

下面再看个有初始化参数的例子:

class Direction:def __init__(self, horizontal=None, vertical=None):self.horizontal = horizontalself.vertical = verticaldef direction(loc):match loc:case Direction(horizontal='east', vertical='north'):print('You towards northeast')case Direction(horizontal='east', vertical='south'):print('You towards southeast')case Direction(horizontal='west', vertical='north'):print('You towards northwest')case Direction(horizontal='west', vertical='south'):print('You towards southwest')case Direction(horizontal=None):print(f'You towards {loc.vertical}')case Direction(vertical=None):print(f'You towards {loc.horizontal}')case _:print('Invalid Direction')d1 = Direction('east', 'south')d2 = Direction(vertical='north')d3 = Direction('centre', 'centre')# 应用direction(d1)# You towards southeastdirection(d2)# You towards northdirection(d3)# Invalid Direction

匹配位置属性

你可以在某些为其属性提供了排序的内置类(例如 dataclass)中使用位置参数。

from dataclasses import dataclass@dataclassclass Point:x: inty: int

你也可以通过在你的类中设置 __match_args__ 特殊属性来为模式中的属性定义一个专门的位置。

class Point:__match_args__ = ("x", "y")x: inty: int

以下模式是等价的:

Point(1, var)Point(1, y=var)Point(x=1, y=var)Point(y=var, x=1)

都是将 y 属性绑定到 var 变量。

匹配内建类

相当于在解构赋值时加了一层校验,只有符合类型要求的才会匹配成功。

def match_type(arg):match arg:case [int(), int(a), str()]:print(a)case list(l):print(l)case {"one": str(b)}:print(b)match_type([1, 2, "3"])# 2match_type([1, 2, 3])# [1, 2, 3]match_type({"one": "1"}) # 1

嵌套模式

模式可以任意地嵌套。 例如,如果我们的数据是由点组成的短列表(类似 [Point(x1, y1), Point(x2, y2)] 形式),则它可以这样被匹配:

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 _:print("列表中还有其他内容")

或者:

def match_multiple(arg):match arg:case ["age", *l]:print(l)case ["language", "C++" | "Java" | "Python", *t]:print(t)match_multiple(["age", 18, 29, 30])# [18, 29, 30]match_multiple(["language", "Java", 2, 3, 4, 5, 6])# [2, 3, 4, 5, 6]match_multiple(["language", "Python", 7, 8, 9, 10, "J"])# [2, 3, 4, 5, 6]

模式捕获

模式捕获是为了在模式匹配成功后,获得该模式或者其子模式的值使用,一般用法是 模式 as 变量名

match arg:case "早" | "中" | "晚" as time:print(time)case "一" | "二" | "三" as number:print(number)case [(a, 2) as t, 3]:print(t)

条件匹配

为模式添加成为守护项的 if 子句。如果守护项的值为假,则 match 继续匹配下一个 case 语句块。注意,值的捕获发生在守护项被求值之前

match point:case Point(x, y) if x == y:print(f"Y=X at {x}")case Point(x, y):print(f"Not on the diagonal")