增加了采用模式加上相应动作的 match 语句 和 case 语句 的形式的结构化模式匹配。 模式由序列、映射、基本数据类型以及类实例构成。 模式匹配使得程序能够从复杂的数据类型中提取信息、根据数据结构实现分支,并基于不同的数据形式应用特定的动作。
语法与操作
模式匹配的通用语法如下:
match subject: case <pattern_1>: <action_1> case <pattern_2>: <action_2> case <pattern_3>: <action_3> case _: <action_wildcard>
match 语句接受一个表达式并将其值与以一个或多个 case 语句块形式给出的一系列模式进行比较。 具体来说,模式匹配的操作如下:
使用具有特定类型和形状的数据 (
subject
)针对
subject
在match
语句中求值从上到下对 subject 与
case
语句中的每个模式进行比较直到确认匹配到一个模式。执行与被确认匹配的模式相关联的动作。
如果没有确认到一个完全的匹配,则如果提供了使用通配符
_
的最后一个 case 语句,则它将被用作已匹配模式。 如果没有确认到一个完全的匹配并且不存在使用通配符的 case 语句,则整个 match 代码块不执行任何操作。
声明性方式
读者可能是通过 C, Java 或 JavaScript (以及其他许多语言) 中的 switch 语句将一个目标 (数据对象) 与一个字面值 (模式) 进行匹配的简单例子了解到模式匹配的概念的。 switch 语句常常被用来将一个对象/表达式与包含在 case 语句中的字面值进行比较。
更强大的模式匹配例子可以在 Scala 和 Elixir 等语言中找到。 这种结构化模式匹配方式是“声明性”的并且会显式地为所要匹配的数据指定条件(模式)。
虽然使用嵌套的“if”语句的“命令性”系列指令可以被用来完成类似结构化模式匹配的效果,但它没有“声明性”方式那样清晰。 相反地,“声明性”方式指定了一个匹配所要满足的条件,并且通过其显式的模式使之更为易读。 虽然结构化模式匹配可以采取将一个变量与一个 case 语句中的字面值进行比较的最简单形式来使用,但它对于 Python 的真正价值在于其针对目标类型和形状的处理操作。
简单模式:匹配一个字面值
让我们把这个例子看作是模式匹配的最简单形式:一个值,即主词,被匹配到几个字面值,即模式。在下面的例子中,status
是匹配语句的主词。模式是每个 case 语句,字面值代表请求状态代码。匹配后,将执行与该 case 相关的动作:
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"
如果传给上述函数的 status
为 418,则会返回 "I'm a teapot"。 如果传给上述函数的 status
为 500,则带有 _
的 case 语句将作为通配符匹配,并会返回 "Something's wrong with the internet"。 请注意最后一个代码块:变量名 _
将作为 通配符 并确保目标将总是被匹配。 _
的使用是可选的。
你可以使用 |
(“ or ”)在一个模式中组合几个字面值:
case 401 | 403 | 404: return "Not allowed"
无通配符的行为
如果我们修改上面的例子,去掉最后一个 case 块,这个例子就变成:
def http_error(status): match status: case 400: return "Bad request" case 404: return "Not found" case 418: return "I'm a teapot"
如果不在 case 语句中使用 _
,可能会出现不存在匹配的情况。如果不存在匹配,则行为是一个 no-op。例如,如果传入了值为 500 的 status
,就会发生 no-op。
带有字面值和变量的模式
模式可以看起来像解包形式,而且模式可以用来绑定变量。在这个例子中,一个数据点可以被解包为它的 x 坐标和 y 坐标:
# point is an (x, y) tuple match point: case (0, 0): print("Origin") case (0, y): print(f"Y={y}") case (x, 0): print(f"X={x}") case (x, y): print(f"X={x}, Y={y}") case _: raise ValueError("Not a point")
第一个模式有两个字面值 (0, 0)
,可以看作是上面所示字面值模式的扩展。接下来的两个模式结合了一个字面值和一个变量,而变量 绑定 了一个来自主词的值(point
)。 第四种模式捕获了两个值,这使得它在概念上类似于解包赋值 (x, y) = point
。
模式和类
如果你使用类来结构化你的数据,你可以使用类的名字,后面跟一个类似构造函数的参数列表,作为一种模式。这种模式可以将类的属性捕捉到变量中:
class Point: x: int y: int def location(point): match point: case Point(x=0, y=0): print("Origin is the point's location.") case Point(x=0, y=y): print(f"Y={y} and the point is on the y-axis.") case Point(x=x, y=0): print(f"X={x} and the point is on the x-axis.") case Point(): print("The point is located somewhere else on the plane.") case _: print("Not a point")
带有位置参数的模式
你可以在某些为其属性提供了排序的内置类(例如 dataclass)中使用位置参数。 你也可以通过在你的类中设置 __match_args__
特殊属性来为模式中的属性定义一个专门的位置。 如果它被设为 ("x", "y"),则以下模式均为等价的(并且都是将 y
属性绑定到 var
变量):
Point(1, var) Point(1, y=var) Point(x=1, y=var) Point(y=var, x=1)
嵌套模式
模式可以任意地嵌套。 例如,如果我们的数据是由点组成的短列表,则它可以这样被匹配:
match points: case []: print("No points in the list.") case [Point(0, 0)]: print("The origin is the only point in the list.") case [Point(x, y)]: print(f"A single point {x}, {y} is in the list.") case [Point(0, y1), Point(0, y2)]: print(f"Two points on the Y axis at {y1}, {y2} are in the list.") case _: print("Something else is found in the list.")
复杂模式和通配符
到目前为止,这些例子仅在最后一个 case 语句中使用了 _
。 但通配符可以被用在更复杂的模式中,例如 ('error', code, _)
。 举例来说:
match test_variable: case ('warning', code, 40): print("A warning has been received.") case ('error', code, _): print(f"An error {code} occurred.")
在上述情况下,test_variable
将可匹配 ('error', code, 100) 和 ('error', code, 800)。
约束项
我们可以向一个模式添加 if
子句,称为“约束项”。 如果约束项为假值,则 match
将继续尝试下一个 case 语句块。 请注意值的捕获发生在约束项被求值之前。:
match point: case Point(x, y) if x == y: print(f"The point is located on the diagonal Y=X at {x}.") case Point(x, y): print(f"Point is not on the diagonal.")
其他关键特性
一些其他关键特性:
类似于解包赋值,元组和列表模式具有完全相同的含义,而且实际上能匹配任意序列。 从技术上说,目标必须为一个序列。 因而,一个重要的例外是模式不能匹配迭代器。 而且,为了避免一个常见的错误,序列模式不能匹配字符串。
序列模式支持通配符: [x, y, *rest]
和 (x, y, *rest)
的作用类似于解包赋值中的通配符。 在 *
之后的名称也可以为 _
,因此 (x, y, *_)
可以匹配包含两个条目的序列而不必绑定其余的条目。
映射模式: {"bandwidth": b, "latency": l}
会从一个字典中捕获 "bandwidth"
和 "latency"
值。 与序列模式不同,额外的键会被忽略。 也支持通配符 **rest
。 (但 **_
是冗余的,因而不被允许。)
子模式可使用 as
关键字来捕获:
case (Point(x1, y1), Point(x2, y2) as p2): ...
x1, y1, x2, y2 等绑定就如你在没有 as
子句的情况下所期望的,而 p2 会绑定目标的整个第二项。
大多数字面值是按相等性比较的。 但是,单例对象 True
, False
和 None
则是按标识号比较的。
命名常量也可以在模式中使用。 这些命名常量必须为带点号的名称以防止常量被解读为捕获变量:
from enum import Enum class Color(Enum): RED = 0 GREEN = 1 BLUE = 2 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 :(")