Structural Pattern Matching(match 语句)
Structural Pattern Matching: 翻译过来应该是 结构化的模式匹配。从python 3.10开始提供了match statement。它远比简单的其它语言中的那种switch语句功能强大的多。
通过一个例子来了解一下这种语句的用法。
假设我们有一个函数,用来区分用户做的操作,并将其打印出来。 我们使用json结构的字符串来做为用户操作的内容并传到这个函数中。目前这个函数只区分用户敲击键盘,及单击鼠标。
下面的函数内部使用if else语句来判断输入的json字符串应该匹配到哪一种用户操作。判断的条件足够的严谨!
import json
def log(event):
parsed_event = json.loads(event)
if (
"keyboard" in parsed_event and
"key" in parsed_event["keyboard"] and
"code" in parsed_event["keyboard"]["key"]
):
code = parsed_event["keyboard"]["key"]["code"]
print(f"Key pressed: {code}")
elif (
"mouse" in parsed_event and
"cursor" in parsed_event["mouse"] and
"screen" in parsed_event["mouse"]["cursor"]
):
screen = parsed_event["mouse"]["cursor"]["screen"]
if isinstance(screen, list) and len(screen) == 2:
x, y = screen
print(f"Mouse cursor: x={x}, y={y}")
else:
print("Unknown event type")
else:
print("Unknown event type")
event1 = '''{"keyboard": {"key": {"code": "Enter"}}}'''
event2 = '''{"mouse": {"cursor": {"screen": [600, 900]}}}'''
event3 = '''{"mouse": {"click": {"screen": [600, 900]}}}'''
log(event1) # Key pressed: Enter
log(event2) # Mouse cursor: x=600, y=900
log(event3) # Unknown event type
让我们看一下如果我们使用match statement,如何来改写上面的函数:
import json
def log(event):
match json.loads(event):
case {"keyboard": {"key": {"code": code}}}:
print(f"Key pressed: {code}")
case {"mouse": {"cursor": {"screen": [x, y]}}}:
print(f"Mouse cursor: {x=}, {y=}")
case _:
print("Unknown event type")
event1 = '''{"keyboard": {"key": {"code": "Enter"}}}'''
event2 = '''{"mouse": {"cursor": {"screen": [600, 900]}}}'''
event3 = '''{"mouse": {"click": {"screen": [600, 900]}}}'''
log(event1) # Key pressed: Enter
log(event2) # Mouse cursor: x=600, y=900
log(event3) # Unknown event type
我们可以明显的看到,实现了同样的判断逻辑,我们使用match语句后,代码非常精简,并且更直观易读。
上面的示例体现了match语句用于匹配数据字典格式数据的能力,就像在声明一个字典的结构以及它要有的某些key和value,并且还可以把我们感兴趣的部分数据方便的提取出来。
现在,假设我们要用一个列表中包含三个值,来表示空间坐标系中的一个点,这个点必须在z轴上,也就是第三个数字为0。 前两个数字可以是float,也可以是int。
下面我们用传统的if...else语句来做一个逻辑严谨的匹配规则判断:
# subject = ['a'] # 匹配不上
# subject = [9.3, 10, 0, 88] # 匹配不上
subject = [9.3, 10, 0] # 匹配成功
if isinstance(subject, list) and len(subject) == 3:
if (
isinstance(subject[0], int | float) and
isinstance(subject[1], int | float) and
subject[2] == 0
):
x, y, _ = subject
print(f"Point({x=}, {y=})")
# 输出: Point(x=9.3, y=10)
接下来我们用match语句来实现上述匹配规则:
# subject = ['a'] # 匹配不上
# subject = [9.3, 10, 0, 88] # 匹配不上
subject = [9.3, 10, 0] # 匹配成功
match subject:
case list([int() | float() as x, int() | float() as y, 0]):
print(f"Point({x=}, {y=})")
# 输出: Point(x=9.3, y=10)
可以看到,我们使用了list()这种类定义的形式来做模式匹配。
注意,match后面接的是表达式,但case后面接的是模式(pattern),模式是不会运行的,比如list()不会真的生成一个空列表,而只代表其结构是一个列表。
我们用dataclass来说明这个问题:
from dataclasses import dataclass
@dataclass
class User:
name: str
def __post_init__(self):
print(f"Created a new user named {self.name}")
match User("Alice"):
case User("Bob"):
print('This is Bob')
输出结果:
Created a new user named Alice
从输出结果可以看出,只执行了一次__post_init__
方法 ,说明 case User("Bob"):
这句并未真正也创建一个dataclass的实例。
下面介绍一些基本的匹配模式:
Literal Pattern: 字面量模式, 字符串,整数,浮点数,布尔值等等。 但不包括复杂类型,比如sequence,dictionary,class等等。
除布尔值外,其他字面量都使用等值方式去匹配。 比如下面函数, 18可以匹配到18.0
def is_adult(age):
match age:
case 18:
return True
case _:
return False
is_adult(18) # True
is_adult(18.0) # True
在python中,True和False是两个单例对象,None也一样。 所以一般判断一个值是否是它们三者中的一个,都用"is" 操作符来判断。并且,True和False是int类的子类。并且其值分别是1和0。
可以用下面代码进行验证:
print(1 == True) # True
print(2 == True) # False
print(0 == False) # True
print(-1 == False) # False
下面是一个简单的例子,case后面接的是True/False字面量,需要使用is操作符进行判断:
def is_true(value):
match value:
case False:
print(False)
case True:
print(True)
case _:
print('未匹配')
is_true(1) # False
is_true(0) # False
is_true(False) # False
is_true(True) # True
下面是另一个例子,case后面是数字,就使用==
操作符来判断。注意,决定判断使用is
还是==
操作符是看case后面的字面量是不是True和False,而不是看match后面的表达式是什么类型。
def is_true(value):
match value:
case 0:
print(0)
case 1:
print(1)
case False:
print(False)
case True:
print(True)
case _:
print('未匹配')
is_true(0) # 0
is_true(1) # 1
is_true(False) # 0
is_true(True) # 1
从上面的执行结果看,虽然当match后面放了True或False时,case块依次比对,前两个是整数字面量0和1,所以使用==操作符匹配,又因为True和False是int类的子类,其值等价于1和0,所以就能直接匹配上了。
Value Pattern: 值模式,用于使用一个对象属性值来进行匹配。
很多时候,我们想指定我们模式的值是一个变量,而不是字面量,这时就可以使用value pattern,而不能直接使用一个变量。因为变量会被当做capture pattern。
下面的例子可以看到,v_cmd变量的值被覆写成了match后面subject的值。 这就是因为case后面放变量会捕获所有情况,是一个capture pattern.
COMMAND = 'exit'
v_cmd = 'help'
match COMMAND:
case 'hello':
print('hello')
case v_cmd:
print(v_cmd)
输出结果:
exit
如果实际应用中,我们case后面跟的不是一个字面量,而是一个在程序运行时过程中才能决定的值,我们可以把它放在一个命名空间里,比如class, enum, module等等,总之可以使用点操作符访问得到,这种模式就是value pattern。
我们将上面的例子改写成value pattern。
v_cmd = 'help'
class CMD:
prop = v_cmd
def match_cmd(p_command):
match p_command:
case 'hello':
print('hello')
case CMD.prop:
print(v_cmd)
case _:
print('not matche')
match_cmd('exit') # not matche
match_cmd('help') # help
Capture Pattern: 捕获模式,顾名思义就是捕获所有情况,无论match后面的subject是什么,都能匹配到。case后面直接跟一个变量,捕获到的值就放在这个变量中,其实也就是match后面的表达式的值。
下面这个简单的例子可以看到,最后的case block相当于直接声明了一个变量,由于前面的case block都没有匹配上,所以最终值就放在这个var变量中。
match '123':
case '456':
print('456')
case 'abc':
print('abc')
case var:
print('capter pattern:', var)
print('var is', var)
输出结果:
capter pattern: 123
var is 123
那如果前面的case block先匹配上了,则捕获模式中的变量就不会被定义,match语句之后如果要引用那个变量,就会报错说变量未定义。
match 'abc':
case '456':
print('456')
case 'abc':
print('abc')
case var:
print('capter pattern:', var)
print('var is', var)
输出结果报错了:
abc
print('var is', var)
^^^
NameError: name 'var' is not defined. Did you mean: 'vars'?
这里顺便要提一下通配符模式:
Wildcard pattern: , 与capture pattern一样,它也会捕获所有subject,但是""在这里只是一种特殊关键字(soft keyword),只在当前范围有这个特殊意义。并不会像capter pattern一样自动创建一个名为"_"的变量。
match '789':
case '456':
print('456')
case 'abc':
print('abc')
case _:
print('capture pattern')
print(_)
输出结果报错:
capture pattern
NameError: name '_' is not defined
由于capture pattern和wildcard pattern的这种特殊性,所以在一个match语句中,不能出现多个capture pattern,或多个wildcard pattern,或者两者混合。 但是在capture pattern后面可以加上条件语句,称之为guard,这样就可以允许出现多个capture pattern。
Guard: 用于pattern之后的判断语句,只有在pattern成功匹配后才执行,如果返回True,则最终执行这个pattern对应的case block。 否则,继续匹配下一个pattern。
先看一个guard简单示例:
match 'abc':
case '456':
print('456')
case 'abc' if 1 > 2:
print('abc')
case 'abc' if 1 < 2:
print('abc and 1 < 2')
case _:
print('capture pattern')
输出结果:
abc and 1 < 2
下面语句直接提示有语法错误,因为出现了两个capter pattern的case block。
match 'abc':
case '456':
print('456')
case var1:
print('var1')
case var2:
print('var2')
而像下面这样写就不会报错,并且运行结果会输出"var2"。:
match 'abc':
case '456':
print('456')
case var1 if 1 > 2:
print('var1')
case var2 if 1 < 2:
print('var2')
其实我们还可以在不同的case block中使用相同变量名的capture pattern。
compound pattern 和 subpattern: 顾名思义,就是指一个case语句后面的pattern不是一个简单的pattern,还是有多个pattern复合构造成。其中的各个子pattern也就被称为subpattern了。
下面例子中的case block里包含了literal pattern, wildcard pattern, class pattern, capture pattern。
from dataclasses import dataclass
from typing import Self
@dataclass
class Command:
name: str
options: tuple[str, ...]
subcommand: None | Self = None
command = Command(
name="git",
options=(),
subcommand=Command(
name="commit",
options=("-a", "-m", "'Initial commit'"),
)
)
match command:
case Command("git", _, Command(subcommand, options)):
print(f"{subcommand=}\n{options=}")
Class Pattern: 类模式。用于精确匹配subject的数据类型。 就像是给对象进行解构一样,也是使用类名加括号的形式,同时也可以使用capture pattern来捕获对象属性值。需要注意的是,pattern里类名后面的括号里放的是类的属性,而不是类的__init__
方法里的参数。
下面的示例展示了,当我只关心subject的数据类型的时候的用法。 用于表示日期的值可能是一个数值,一个日期对象,或者一个字符串。
from datetime import UTC, datetime
def parse(value):
match value:
case datetime():
return value.astimezone(UTC)
case int() | float():
return datetime.fromtimestamp(value, UTC)
case str():
return datetime.fromisoformat(value).astimezone(UTC)
case _:
raise TypeError(f"Unsupported type: {type(value)}")
parse(-14182967.876544) # 1970-06-14 03:42:47.876544+00:00
parse("1969-07-20T20:17:12.123456+00:00") # 1969-07-20 20:17:12.123456+00:00
下面的例子通过使用命名参数来更精确的匹配subject,同时可使用了capture pattern捕获到了subject对象的属性值。
import datetime
def aoc_status(date):
match date:
case datetime.date(year=_, month=12, day=1):
print("Advent of Code starts today!")
case datetime.date(year=_, month=12, day=day) if day <= 25:
print(f"{25 - day} days until the end of AoC.")
case _:
print("Sorry, no Advent of Code today.")
aoc_status(datetime.date.today()) # Sorry, no Advent of Code today.
aoc_status(datetime.date(2024, 12, 1)) # Advent of Code starts today!
aoc_status(datetime.date(2024, 12, 2)) # 23 days until the end of AoC.
除非类实现了__match_args__
方法,否则class pattern里如果带参数,都使用关键字参数。 像是我们自定义的类,即便自定义的__init__
方法只有一个参数,在class pattern中也要使用key-word方式表示。但是对于那些built-in type的构造函数,比如int(),str()等等,它们有单个参数的构造方法,这时则可以在class pattern中使用位置参数的方式。
def length(value):
match value:
case str(text):
return len(text)
case int(number):
return number.bit_length()
length("255") # 3
length(255) # 8
Sequence Pattern: 序列模式用于匹配常见的list, tuple, range, collections.namedtuple等序列,但并不是支持所有iterable类型。可以使用星号来代表序列中多个元素,用法类似于解包赋值。中括号和小括号都可用于标记序列模式,但如果要匹配的序列只有一个元素,这时如果使用小括号,则后面要跟一个逗号。例如:(item,)
command = ['git', 'commit', '-a', '-m', "'Initial commit'"]
match command:
case ["git"]:
print("Showing the list of available Git subcommands.")
case ["git", "--version"]:
print("Showing the version of your Git client.")
case [_, subcommand, *options]:
print(f"{subcommand=}, {options=}")
# 输出结果: ubcommand='commit', options=['-a', '-m', "'Initial commit'"]
上面的示例中使用中括号标识出是sequence pattern,并且内部使用了wildcard pattern,literal pattern, 解包写法,和capture pattern。
下面我们再展示一下使用小括号,及不使用括号,但使用多个pattern中间用逗号分隔的方式来匹配。
lst = [1,2,3]
match lst:
case (1,2,v):
print(v) # 3
match lst:
case 1,v,3:
print(v) # 2
其实在python中,小括号有时用于标记元组,有时又只是一种分组符。下面的示例可以看出,当我们想用字面量定义一个只有一个元素的元组时,元素后要加个逗号。 否则它就不是元组。
v1 = ('a')
v2 = ('a',)
print(type(v1), v1) # <class 'str'> a
print(type(v2), v2) # <class 'tuple'> ('a',)
我们看下如何匹配只有一个元素的sequence,使用sequence pattern。
tup = ('abc',)
match tup:
case v,:
print(type(v), v)
match tup:
case (v,):
print(type(v), v)
match tup:
case v:
print(type(v), v)
match tup:
case (v):
print(type(v), v)
match tup:
case [v]:
print(type(v), v)
输出结果:
<class 'str'> abc
<class 'str'> abc
<class 'tuple'> ('abc',)
<class 'tuple'> ('abc',)
<class 'str'> abc
从输出结果来看,使用中括号来标识sequence pattern是最稳妥不容易出错的方式。 使用不带逗号的单个变量实际上是capture pattern,会捕获所有数据类型的。
python中,空元组的字面量是(),这时倒不能加个逗号。 下面的例子中,两个case的pattern其实是等效的。因为中括号的在上面,所以就先执行了case []。
subject = ()
print(type(subject), subject)
print('-' * 25, '分隔线', '-' * 25)
match subject:
case []:
print("This will match the subject if it's an empty sequence.")
case ():
print("This is equivalent to the above.")
输出结果:
<class 'tuple'> ()
------------------------- 分隔线 -------------------------
This will match the subject if it's an empty sequence.
再看一些*号在squence pattern中的灵活用法:
subject = list('abcdefg')
match subject:
case [v1,v2, *_]:
print(f"v1={v1}, v2={v2}")
print('-' * 25, '分隔线', '-' * 25)
match subject:
case [*all_items]:
print(all_items)
print('-' * 25, '分隔线', '-' * 25)
match subject:
case first, *middle, last:
print(f"{first=}")
print(f"{middle=}")
print(f"{last=}")
print('-' * 25, '分隔线', '-' * 25)
输出结果:
v1=a, v2=b
------------------------- 分隔线 -------------------------
['a', 'b', 'c', 'd', 'e', 'f', 'g']
------------------------- 分隔线 -------------------------
first='a'
middle=['b', 'c', 'd', 'e', 'f']
last='g'
------------------------- 分隔线 -------------------------
如果不使用*号,则sequence pattern的元素个数要与subject一致,否则就匹配不上。
cursor_position = (765, 432, 0)
match cursor_position:
case [x, y]:
print(f"{x=}, {y=}")
case _:
print("Cursor position is not a two-tuple.")
# 输出结果: Cursor position is not a two-tuple.
sequence pattern匹配任何支持的sequence类型,但如果我们要精确匹配具体的sequence类型,这时就需要借助class pattern。
def flip(pair):
match pair:
case list([x, y]):
return [y, x]
case tuple([x, y]):
return y, x
case _:
raise TypeError("Unsupported type")
flip([1, 2]) # [2, 1]
flip((1, 2)) # (2, 1)
Mapping Pattern: 用于匹配映射关系(key-value)的数据,比如dictionary, collections.OrderedDict, collections.defaultdict等等。 匹配时可以不提供所有的key以及所有嵌套层级。但key不能是captural pattern,可以是literal pattern或value pattern。
下面的示例之前使用class pattern进行过演示,现在也可以使用mapping pattern来处理。
from dataclasses import asdict, dataclass
from typing import Self
@dataclass
class Command:
name: str
options: tuple[str, ...]
subcommand: None | Self = None
command = Command(
name="git",
options=(),
subcommand=Command(
name="commit",
options=("-a", "-m", "'Initial commit'"),
)
)
match asdict(command):
case {"subcommand": {"options": options, "name": "commit"}}:
print(options) # ('-a', '-m', "'Initial commit'")
与sequence patterns不同的是,case []可以用于匹配空的sequence,但是case {}可以匹配所有支持的mapping数据,即便数据不是一个空的mapping对象。比如空字典。
match asdict(command):
case {}:
print('can match any supported mapping object, include empty object')
我们可以用**var
这种语法获取除个别key-value以外的数据。比如:
match asdict(command):
case {"subcommand": _, **rest}:
print(rest) # {'name': 'git', 'options': ()}
OR Pattern: 就是合并几个basic pattern。它们之间的或的关系。 如果子pattern中有capture pattern,则每个子pattern都要有相同的capture pattern,即变量名及个数都要一致。
下面是一个简单的例子,subpatterns是literal pattern。
command = 'exit'
match command:
case 'exit' | 'quit':
print('exit app')
case _:
print('others command')
下面例子中subpatterns是class pattern,里面又含有capture pattern,并且capture pattern的名称和个数都得一致,否则会报错,但位置可以不一致。
from dataclasses import dataclass
@dataclass
class User():
name: str
age: int
user1 = User("Harry", 10)
user2 = User("Roland", 40)
match user1:
case User(name='Harry', age=var) | User(name=var, age=40):
print('Harry or Roland')
case _:
print('other user')
# 输出结果: Harry or Roland
AS Pattern: 用于将匹配到的内容绑定给一个变量。 比如,当我们用OR Pattern来组合多个literal pattern时,我们不知道最终是哪个literal pattern匹配成功了,我们可以用as来将其值绑定到一个变量,以便后续使用。
match command:
case 'exit' | 'quit' as cmd:
print(f'cmd is {cmd}')
case _:
print('others command')
# 输出结果:cmd is exit
下面例子将class pattern匹配到的具体对象绑定到了变量。
@dataclass
class User():
name: str
age: int
user1 = User("Harry", 10)
user2 = User("Roland", 40)
match user1:
case User(name='Harry', age=var) | User(name=var, age=40) as user:
print(user)
case _:
print('other user')
# 输出结果: User(name='Harry', age=10)
Group Pattern: 用小括号包裹pattern,纯粹是为了让复杂的pattern的结构更清晰。 注意,小括号还用在sequence pattern中,括号内为空,代表是空的sequence pattern,当代表只有一个元素的sequence时,后面要有一个逗号。
下面是一个表示经纬坐标的pattern,里面的小括号是为了区分各子组。像第一个小括其实是可以拿掉的,意义等价。
match subject:
case [(latitude), (("N" | "S") | ("W" | "E")) as hemisphere]:
...
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)