写点什么

Python3.10 中的结构化模式匹配语法

用户头像
★忆先★
关注
发布于: 2021 年 06 月 17 日

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,404418,匹配成功的话就会执行对应的逻辑,_作为兜底匹配所有情况,在本例中如果传的status 不能匹配前面三个值的话,例如status500,就会返回"Something's wrong with the Internet"。如果不使用_的话,传status500的时候所有case语句都匹配失败,程序就会执行到match case后面的逻辑,在这个例子中就是函数执行结束,没有返回。


可以使用|操作合并需要做相同处理的字面量,类似于if语句中的or


case 401 | 403 | 404:    return "Not allowed"
复制代码

更复杂(且实用)的匹配

只是匹配字面量的话match case语法实际上与if else语法没有太大的区别。然而模式匹配真正发挥作用的地方不在于此,在我看来,模式匹配语法的关键在于模式二 字。


在 Python 3.10 之前,我们已经可以对列表、元组等可迭代对象进行简单的解构赋值了。


>>> x, y = (0, 1)>>> x0>>> y1
>>> a, *b, c = range(5)>>> a0>>> b[1, 2, 3]>>> c4
>>> a, (b, c) = (0, (1, 2))>>> a0>>> b1>>> c2
复制代码


在此基础之上,模式匹配语法支持了更多种形式的模式,可以灵活应用于不同场景。


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=0printColor(colorB) # r=255 g=255 b=0 a=128printColor(colorC) # color='RED'printColor(colorD) # Not A ColorprintColor(colorE) # Not A ColorprintColor(colorF) # Not A Color
复制代码


上述代码示例中的printColor函数的作用是解析颜色并打印,函数中匹配了四个模式:


  1. r, g, b:三个元素的列表或者元组或者其他可迭代对象,对应颜色的 RGB 值

  2. r, g, b, a:四个元素的列表或者元组或者其他可迭代对象,对应颜色的 RGB 值以及透明度 Alpha 值

  3. str():str 类型对象,表示颜色的名字

  4. _:其他情况,打印'Not A Color'


对应到colorAcolorF六个变量:


  1. colorAcolorB分别是三个元素的元组和四个元素的数组,匹配前两种模式,打印出对应的颜色值。

  2. colorC和是一个字符串,匹配第三种模式,打印出颜色的名字RED

  3. colorDcolorE分别是五个元素的元组和两个元素的数组,元素个数与前两种模式不一致,也不是str类型,匹配到_分支,打印'Not A Color'

  4. 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@dataclassclass 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 originprint(describe_point(Point2D(3, 0))) # The point is in the horizontal axis, at x = 3print(describe_point(Point2D(0, 4))) # The point is in the vertical axis, at y = 4print(describe_point(Point2D(5, 5))) # The point is along the x = y line, with x = y = 5print(describe_point(Point2D(9, -9))) # The point is along the x = -y line, with x = 9 and y = -9print(describe_point(Point2D(1, 2))) # The point is at (1, 2)
复制代码


describe_point函数中我们直接匹配了一个Point2D对象的x属性和y属性的不同情形,由于最后一个条件case Point2D(x, y)已经匹配了xy的所有可能性 ,所以不需要匹配_通配符分支(假设传入的都是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 FormatparseResp({'success': True}) # Exception: API Error: Invalid Format
复制代码


parseResp函数会解析dict类型的response对象,successTrue时会返回datasuccessFalse时会抛出异常记录错误信息和错误码,当response不符合期望格式时会抛出非法格式的异常(包括successTrue但没有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: 2describe_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: 2describe_list([1]) # Only one element: 1describe_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))

发布于: 2021 年 06 月 17 日阅读数: 11
用户头像

★忆先★

关注

还未添加个人签名 2018.11.12 加入

Python后端开发

评论

发布
暂无评论
Python3.10中的结构化模式匹配语法