1.问题引入
假设我们有这样一个动物类,示例代码如下所示:
from typing import Optionalclass Animal():def __init__(self, name:str):self.name = namedef __str__(self)->str:return self.name.upper()class Cat(Animal):passclass Dog(Animal):passclass Tiger(Animal):pass
现在有这样一个需求,根据传入的动物名称,调用不同的类进行初始化,我们可以有以下几种方式来处理。
1.1 使用if语句
在Python中,在进行多分支判断时,使用最多的就是if-elif-else
。示例代码如下所示:
def animal_factory_use_if(animal_type:str)->Optional[Animal]:if animal_type.lower() == "cat":return Cat(name="Cat")elif animal_type.lower() == "dog":return Dog(name="Dog")elif animal_type.lower() == "tiger":return Tiger(name="Tiger")else:return None
1.2 使用字典
除了使用if
语句,我们也可以使用字典
,示例代码如下所示:
def animal_factory_use_dict(animal_type:str)->Optional[Animal]:# 构造字典数据animal_dict={"cat":Cat(name="Cat"),"dog":Dog(name="Dog"),"tiger":Tiger(name="Tiger")}return animal_dict.get(animal_type.lower())
1.3 使用match
对于这种多分支判断情况,其他编程语言,例如Java、C#、Go等,除if语句之外,还提供了switch-case
结构,那Python有没有类似的结构来实现类似的功能呢?
其实在Python 3.10
版本中提供了 match-case
特性,先看看示例代码如下所示:
def animal_factory_use_match_case(animal_type:str)->Optional[Animal]:match animal_type.lower():case "cat":return Cat(name="Cat")case "dog":return Dog(name="Dog")case "tiger":return Tiger(name="Tiger")case _:return None
match 后面跟要匹配的变量,case跟不同的条件,再之后跟符合条件的执行语句,最后一个下划线表示缺省匹配,如果前面的case都没有匹配的项,就执行这个case,相当于之前的else。是不是跟Java、C#、Go语言里面的 switch-case
很像? 但match-case
能做的事情是超过switch-case
的。它支持更加复杂的模式匹配。让我们来一起学习吧。
2. match-case
2.1 概述
为解决前面提到的问题,在Python 3.10
中引入了非常强大的match-case
语法,也称为结构模式匹配(Structural Pattern Matching),这是一种全新的流程控制语句,允许根据值的结果执行不同的代码块。
2.2 基本概念
2.2.1 match语法
match-case
语法类似于其他编程语言的switch
语句,但功能更为强大,允许根据值的结果执行不同的代码块。
2.2.2 为什么使用match
- 简洁性:使得多重条件判断更加清晰和易读
- 可扩展性:支持复杂的结构和类型匹配
- 功能强大:可对序列、字典、类等进行解构和匹配
2.2.3 语法结构
基本语法结构如下所示:
match value:case pattern1:# 执行代码块-1case pattern2:# 执行代码块-2case _:# 执行默认代码块
match value
: 要进行匹配的值case pattern
: 匹配的模式case _
: 匹配任何值,相当提前预设一种场景
3. 基本使用
3.1 字面值模式
在字面值模式中,可以使用Python自带的数据结构,如字符串、数字、布尔值和None等,示例代码如下所示:
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 匹配成功,则不会执行任何分支
3.2 多模式匹配
可以使用 |(表示或)在一个模式中组合几个字面值,示例代码如下所示:
def http_error_combine(status):match status:case 401 | 403 | 404:return "Not allowed"case 200 | 202 :return "request successful"case 301 | 302 | 304:return "redirect"case 500 | 502:return "server error"case _:return "Something's wrong with the internet"
3.3 序列模式
在match-case
中,也可使用解包赋值的方式来绑定变量,示例代码如下所示:
def print_point(point:Tuple[int])->str: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):表示有两个字面值,可以理解为字面值模式的扩展
- (0, y)、(x, 0):表示结合了一个字面值模式和一个变量,而变量则是绑定了point的一值
- (x, y):表示捕获了两个值,在概念与解包赋值相同,即(x, y) = point
除了在match-case
中使用元组之外,还可以使用列表,示例代码如下所示:
def match_sequence(sequence:Sequence):match sequence:case [1,2]:print(f"条件:[1,2] 输入{sequence} 结果:{[1,2]}")case [1,(x,y)]:print(f"条件:[1,(x,y)] 输入{sequence} 结果:x={x},y={y}")case [x,y,z]:print(f"条件:[x,y,z] 输入{sequence} 结果:x={x},y={y},z={z}")case [x,*y,z]:print(f"条件:[x,*y,z] 输入{sequence} 结果:x={x},y={y},z={z}")case [x,[*y,z]]:print(f"条件:[x,[*y,z]] 输入{sequence} 结果:x={x},y={y},z={z}")case _:print("Not a sequence")if __name__ == "__main__":match_sequence([1,2])match_sequence([1,(2,3)])match_sequence([1,{4,5}])match_sequence([1,2,3])match_sequence([1,2,3,4,5])match_sequence([1,100,99,[2,3,4,5],9999])
代码运行结果如下所示:
条件:[1,2] 输入[1, 2] 结果:[1, 2]
条件:[1,(x,y)] 输入[1, (2, 3)] 结果:x=2,y=3
条件:[x,*y,z] 输入[1, {4, 5}] 结果:x=1,y=[],z={4, 5}
条件:[x,y,z] 输入[1, 2, 3] 结果:x=1,y=2,z=3
条件:[x,*y,z] 输入[1, 2, 3, 4, 5] 结果:x=1,y=[2, 3, 4],z=5
条件:[x,*y,z] 输入[1, 100, 99, [2, 3, 4, 5], 9999] 结果:x=1,y=[100, 99, [2, 3, 4, 5]],z=9999
以上语句一些关键特性总结如下所示:
- 与解包赋值类似,元组和列表模式具有完全相同的含义并且在实际结果上都能匹配任意序列,区别是它们不能匹配迭代器或字符串
- 序列模式支持扩展解包
[x,*y,z]
和(x,*y,z)
和相应的解包赋值功能相同,而在 * 后面也可以接 _
以上的示例,可能比较复杂(平时应该也不会这么为难自己),我们可以看看日常使得较多的示例
def match_list_sample(color:str):match color.split():case ["red","green","blue"]:print(f"{color}")case ["purple","yellow","red"]:print(f"{color}")case ["red"]:print(f"{color}")case _:print("Not a color")
3.4 通配符模式
通配符模式更像是一种兜底模式,使用下划线来匹配任何结果,但不绑定变量,示例如下所示:
def match_list_sample(color:str):match color.split():case ["red","green","blue"]:print(f"{color}")case _:print("Not a color")
以上基础模式,还可以有以下的扩展模式
def match_list_sample(color:str):match color.split():case ["red","green","blue"]:print(f"{color}")case [_,_]:print("Not a color")
通配模式中的
case _
也是可以省略的,但在没有任何匹配项,也是什么都不做
3.5 guard 模式
match-case
模式还支持在case后面添加if判断
,示例如下所示:
def guard_sample(number:List[int]):match number:case [x,y] if x>0 and y>0:print("第一象限")case [x,y] if x<0 and y>0:print("第二象限")case [x, y] if x < 0 and y < 0:print("第三象限")case [x, y] if x > 0 and y < 0:print("第四象限")case [_,_]:print("数据错误")
3.6 字典模式
这种模式其实与前面的序列模式类似,只是将序列换成字典做匹配,示例代码如下所示:
def dict_sample(config):match config:case {"env":env,"host":host}:print(f"env:{env},host:{host}")case {"env": env, **params}:print(f"env:{env},params:{params}")case _:print("error")if __name__ == "__main__":dict_sample({"env":"Test","path":"/home/surpass","file":"surpass.xlsx"})dict_sample({"env":"Test","host":"surpassme.net"})
运行结果如下所示:
env:Test,params:{'path': '/home/surpass', 'file': 'surpass.xlsx'}
env:Test,host:surpassme.net
匹配字典和序列不同之处在于,匹配序列时会要求序列的长度,而在匹配字典时仅看key,如果不确定序列长度,则推荐使用字典进行匹配
3.7 AS模式
可以使用as
来捕获子模式,可以理解为多模式匹配的扩展,示例代码如下所示:
def as_sample(city:str):match city:case ("Shanghai" | "Jiangsu" | "Zhejiang" | "Anhui") as area:print(f"我将去往华东-{area}")case ("Hubei" | "Hunan" | "Henan" | "Jiangxi") as area:print(f"我将去往华中-{area}")case ("Guangdong" | "Guangxi" | "Hainan") as area:print(f"我将去往华南-{area}")case _:print("error")if __name__ == "__main__":as_sample("Shanghai")as_sample("Hainan")
运行结果如下所示:
我将去往华东-Shanghai
我将去往华南-Hainan
3.8 类模式
case后面还支持类进行判断,示例代码如下所示:
def class_match_sample(obj:Click):match obj:case Click(position=(0,0),button="Enter"):print(f"position is (0,0),button is Enter")case Click(position=(100, 200),button="Esc"):print(f"position is (100, 200),button is Esc")case _:print("error")if __name__ == "__main__":class_match_sample(Click((0, 0), "Enter"))class_match_sample(Click((100,200),"Esc"))
运行结果如下所示:
position is (0,0),button is Enter
position is (100, 200),button is Esc
但在使用类模式,位置需要确定,因此在case后的需要使用命名关键字参数传参,不然会出现如下所示的报错:
def class_match_error_sample(obj:Click):match obj:case Click((0,0),"Enter"):print(f"position is (0,0),button is Enter")case Click((100, 200),"Esc"):print(f"position is (100, 200),button is Esc")case _:print("error")
如果使用以上的定义时,在运行时会出现以下所示的报错信息:
Traceback (most recent call last):File "C:\Users\Surpass\Documents\PyCharmProjects\TestNote\main.py", line 200, in <module>class_match_error_sample(Click((0, 0), "Enter"))File "C:\Users\Surpass\Documents\PyCharmProjects\TestNote\main.py", line 165, in class_match_error_samplecase Click((0,0),"Enter"):^^^^^^^^^^^^^^^^^^^^
TypeError: Click() accepts 0 positional sub-patterns (2 given)
不过为了解决传参的麻烦,官方提供了另一个方法,即在类里面添加__match_args__
返回一个位置参数的元组,示例代码如下所示:
class Click():__match_args__ = ("position", "button")def __init__(self, position:Tuple[int], button:str):self.position = positionself.button = buttondef class_match_sample(obj:Click):match obj:case Click((0,0),"Enter"):print(f"position is (0,0),button is Enter")case Click((100, 200),"Esc"):print(f"position is (100, 200),button is Esc")case _:print("error")
4.注意事项
4.1 Python版本要求
- match语法仅在Python 3.10 及以上版本才支持,否则会引起语法错误
4.2 通配符使用
- _ 是一个特殊模式,表示匹配任何值
- 如果需要捕获值,需要使用变量名
4.3 匹配的顺序
- 匹配是按顺序进行的,一旦匹配成功,则后面的case将被忽略
- 在使用,建议将更具体的模式放在前面,通用的模式放在后面位置
5.参考资料
- https://docs.python.org/zh-cn/3.13/tutorial/controlflow.html#match-statements
- https://peps.python.org/pep-0636/#composing-patterns
本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注: