Python 3.10 版本还在开发之中,目前释出的 dev 版本实现了新语法特性 Structural Pattern Matching
(PEP 634):可以利用match
语句和case
语句匹配对象的不同模式,并应用不同的行为。
我先前自己尝试体验了一下Structural Pattern Matching
语法(使用pyenv
安装dev
版本 Python 3.10),感觉很好用的,并且有很大的发挥空间。
Structural pattern matching has been added in the form of a match statement and case statements of patterns with associated actions. Patterns consist of sequences, mappings, primitive data types as well as class instances. Pattern matching enables programs to extract information from complex data types, branch on the structure of data, and apply specific actions based on different forms of data.
官方介绍
语法
match subject:
case <pattern_1>:
<action_1>
case <pattern_2>:
<action_2>
case <pattern_3>:
<action_3>
case _:
<action_wildcard>
复制代码
最简单的模式匹配
我们可以使用match 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"
复制代码
上述http_error
函数中,会依次判断status
是否等于400
,404
或418
,匹配成功的话就会执行对应的逻辑,_
作为兜底匹配所有情况,在本例中如果传的status
不能匹配前面三个值的话,例如status
为500
,就会返回"Something's wrong with the Internet"
。如果不使用_
的话,传status
为500
的时候所有case
语句都匹配失败,程序就会执行到match case
后面的逻辑,在这个例子中就是函数执行结束,没有返回。
可以使用|
操作合并需要做相同处理的字面量,类似于if
语句中的or
。
case 401 | 403 | 404:
return "Not allowed"
复制代码
更复杂(且实用)的匹配
只是匹配字面量的话match case
语法实际上与if else
语法没有太大的区别。然而模式匹配真正发挥作用的地方不在于此,在我看来,模式匹配语法的关键在于模式二 字。
在 Python 3.10 之前,我们已经可以对列表、元组等可迭代对象进行简单的解构赋值了。
>>> x, y = (0, 1)
>>> x
0
>>> y
1
>>> a, *b, c = range(5)
>>> a
0
>>> b
[1, 2, 3]
>>> c
4
>>> a, (b, c) = (0, (1, 2))
>>> a
0
>>> b
1
>>> c
2
复制代码
在此基础之上,模式匹配语法支持了更多种形式的模式,可以灵活应用于不同场景。
colorA = (255, 0, 0)
colorB = [255, 255, 0, 128]
colorC = 'RED'
colorD = (1, 1, 1, 1, 1)
colorE = [255, 255]
colorF = 1
def printColor(color: tuple| list | str):
match color:
case r, g, b:
print(f'{r=} {g=} {b=}')
case r, g, b, a:
print(f'{r=} {g=} {b=} {a=}')
case str():
print(f'{color=}')
case _:
print('Not A Color')
printColor(colorA) # r=255 g=0 b=0
printColor(colorB) # r=255 g=255 b=0 a=128
printColor(colorC) # color='RED'
printColor(colorD) # Not A Color
printColor(colorE) # Not A Color
printColor(colorF) # Not A Color
复制代码
上述代码示例中的printColor
函数的作用是解析颜色并打印,函数中匹配了四个模式:
r, g, b
:三个元素的列表或者元组或者其他可迭代对象,对应颜色的 RGB 值
r, g, b, a
:四个元素的列表或者元组或者其他可迭代对象,对应颜色的 RGB 值以及透明度 Alpha 值
str()
:str 类型对象,表示颜色的名字
_
:其他情况,打印'Not A Color'
对应到colorA
到colorF
六个变量:
colorA
和colorB
分别是三个元素的元组和四个元素的数组,匹配前两种模式,打印出对应的颜色值。
colorC
和是一个字符串,匹配第三种模式,打印出颜色的名字RED
。
colorD
和colorE
分别是五个元素的元组和两个元素的数组,元素个数与前两种模式不一致,也不是str
类型,匹配到_
分支,打印'Not A Color'
colorF
是一个整数,匹配到_
分支,打印'Not A Color'
将上述逻辑用传统的if else
语句实现,将会是如下oldPrintColor
的样子:
def oldPrintColor(color: tuple| list | str):
if isinstance(color, tuple | list):
if len(color) == 3:
r, g, b = color
print(f'{r=} {g=} {b=}')
elif len(color) == 4:
r, g, b, a = color
print(f'{r=} {g=} {b=} {a=}')
else:
print('Not A Color')
elif isinstance(color, str):
print(f'{color=}')
else:
print('Not a Color')
复制代码
if else
版本并没有比match case
版本多出太多代码,但也明显不如模式匹配版本直观。
从这个例子我们可以看到模式匹配语法的优势和使用场景:匹配一个对象的多种不同模式,同时进行变量赋值以供后续的逻辑使用。
其他模式匹配语法的用法
模式匹配语法还有更多灵活的用法
匹配自定义类型
我们可以使用模式匹配语法匹配自定义类型的结构。
from dataclasses import dataclass
@dataclass
class Point2D:
x: float
y: float
def describe_point(point: Point):
"""Write a human-readable description of the point position."""
match point:
case Point2D(0, 0):
desc = "at the origin"
case Point2D(0, y):
desc = f"in the vertical axis, at y = {y}"
case Point2D(x, 0):
desc = f"in the horizontal axis, at x = {x}"
case Point2D(x, y) if x == y:
desc = f"along the x = y line, with x = y = {x}"
case Point2D(x, y) if x == -y:
desc = f"along the x = -y line, with x = {x} and y = {y}"
case Point2D(x, y):
desc = f"at {point}"
return "The point is " + desc
print(describe_point(Point2D(0, 0))) # The point is at the origin
print(describe_point(Point2D(3, 0))) # The point is in the horizontal axis, at x = 3
print(describe_point(Point2D(0, 4))) # The point is in the vertical axis, at y = 4
print(describe_point(Point2D(5, 5))) # The point is along the x = y line, with x = y = 5
print(describe_point(Point2D(9, -9))) # The point is along the x = -y line, with x = 9 and y = -9
print(describe_point(Point2D(1, 2))) # The point is at (1, 2)
复制代码
在describe_point
函数中我们直接匹配了一个Point2D
对象的x
属性和y
属性的不同情形,由于最后一个条件case Point2D(x, y)
已经匹配了x
和y
的所有可能性 ,所以不需要匹配_
通配符分支(假设传入的都是Point2D
对象)。
在匹配时进行额外条件判断
我们可以在case
语句中加入额外的条件判断逻辑,此时需要模式匹配成功和条件判断通过时才能通过匹配。
在describe_point
函数中的第四和第五个模式, 我们加入了额外的if
语句来判断Point2D
对象是否在直线x=y
和直线x=-y
上,都不符合的时候才会匹配最后一个模 式case Point2D(x, y)
case Point2D(x, y) if x == y:
desc = f"along the x = y line, with x = y = {x}"
case Point2D(x, y) if x == -y:
desc = f"along the x = -y line, with x = {x} and y = {y}"
case Point2D(x, y):
desc = f"at {point}"
复制代码
匹配字典
我们可以对字典的键值进行匹配。
def parseResp(response:dict):
match response:
case {'success': True,'data': data, **rest}:
return data
case {'success': False, 'code': code, 'msg': msg, **rest}:
raise Exception(f'API Error: {code=} {msg=}')
case _:
raise Exception(f'API Error: Invalid Format')
parseResp({'success': True, 'data': {'result': 1}}) # {'result': 1}
parseResp({'success': False, 'msg': 'msg', 'code': 1}) # raise Exception: API Error: code=1 msg='msg'
parseResp({'foo':'bar'}) # Exception: API Error: Invalid Format
parseResp({'success': True}) # Exception: API Error: Invalid Format
复制代码
parseResp
函数会解析dict
类型的response
对象,success
为True
时会返回data
,success
为False
时会抛出异常记录错误信息和错误码,当response
不符合期望格式时会抛出非法格式的异常(包括success
为True
但没有data
值的情况)
在匹配时使用*
与**
我们在模式匹配时还可以使用*
和**
匹配剩余的元素,值得注意的是剩余的元素数量可能为 0。
def describe_list(x: list):
match x:
case a, b:
print(f'First: {a}, Second: {b}')
case a, *rest:
print(f'First: {a}, Rest: {rest}')
case _:
print(f'Other')
describe_list([1, 2]) # First: 1, Second: 2
describe_list([1]) # First: 1, Rest: []
describe_list([1, 2, 3]) # First: 1, Rest: [2, 3]
复制代码
可以看到在describe_list
函数中,列表x
长度为 1 或 3 时,都可以匹配到case a, *rest
模式。不过我们可以简单的修改一下describe_list
函数,单独匹配只有一个元素的情况。
def describe_list(x: list):
match x:
case a, b:
print(f'First: {a}, Second: {b}')
case a, * rest if not rest:
print(f'Only one element: {a}')
case a, *rest:
print(f'First: {a}, Rest: {rest}')
case _:
print(f'Other')
describe_list([1, 2]) # First: 1, Second: 2
describe_list([1]) # Only one element: 1
describe_list([1, 2, 3]) # First: 1, Rest: [2, 3]
复制代码
有些读者或许想通过这样的方式匹配只有一个元素的情况。
def describe_list(x: list):
match x:
case a:
print(f'Only one element: {a}')
case a, b:
print(f'First: {a}, Second: {b}')
case a, *rest:
print(f'First: {a}, Rest: {rest}')
case _:
print(f'Other')
复制代码
实际上上述实现会无法执行,抛出语法错误。
case a:
^
SyntaxError: name capture 'a' makes remaining patterns unreachable
复制代码
匹配a
和匹配_
实际上是一样的,就是把原始的x
对象赋给了a
或_
,自然是能匹配所有情况,导致后续的模式无法抵达。我们先前之所以用_
实际上是 Python 的一个惯例。
匹配只有一个元素的可迭代对象的另一个方法是将pattern
包裹在()
或者[]
里:
def describe_list(x: list):
match x:
case [a]: # case (a,):
print(f'Only one element: {a}')
case a, b:
print(f'First: {a}, Second: {b}')
case a, *rest:
print(f'First: {a}, Rest: {rest}')
case _:
print(f'Other')
复制代码
总结
本文简单的介绍了 Python 3.10 版本带来的Structural Pattern Matching
模式匹配语法。Python 的模式匹配借鉴了一些其他语言的模式匹配机制,并且维持了 自己的简洁直观的语言风格,弥补了一直来 Python 在相关领域语法的缺失和不足(以前只能用if
语句)。相信在 3.10 版本正式发布并稳定之后,模式匹配语法将会出现在大家的关键业务逻辑中。
[原文](Python 3.10中的模式匹配 | 那时难决 (duyixian.cn))
评论