该特性已经有 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
,False
和None
则是按标识号比较的。
使用 |
在一个模式中还可以组合多个字面值:
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]) # 序列只有一个元素:2
match_sequence([4, 1]) # 序列有两个元素,[4, 1]
match_sequence([1, 2, 3]) # 序列有三个元素,第三个元素为 3
match_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=4
match_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=18
match_dict() # value=v
match_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 = 0
p2.y = 4
p3 = Point()
p3.x = 5
p3.y = 6location(p1) # 这个点不在轴上
location(p2) # Y=4
location(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 southeast
direction(d2) # You towards north
direction(d3) # Invalid Direction
匹配位置属性
你可以在某些为其属性提供了排序的内置类(例如 dataclass
)中使用位置参数。
from dataclasses import dataclass@dataclass
class 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"]) # 2
match_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")