Python3.10重磅新特性:用模式匹配优化代码

2610 views, 2021/10/22 updated   Go to Comments

前几天 Python 3.10 版本发布了,里面就包含了模式匹配这个重磅新特性。

合理运用模式匹配,毫无疑问可以显著优化你的代码。

让我们好好聊聊它。

值模式

下面就是一个简单的模式匹配语句:

flag = 1

match flag:
    case 0:
        print('0')
    case 1:
        print('1')
    case _:
        print('None')


# 输出: 1
  • 关键字 match 后的是被匹配的值
  • 解释器会将此值逐个与关键字 case 后的值做对比,若相等则进入此 case 语句,其他 case 语句不再执行。
  • 因此上面的代码进入了 case 1 ,打印了字符串 ”1“ 。
  • case _ 表示如果以上所有值都没能匹配,那就进入这条语句块。

好家伙,这和 if..elif..else 有区别吗?

把它们放到一起对比:

match flag:
    case 0:
        print('0')
    case 1:
        print('1')
    case 2:
        print('2')
    case _:
        print('None')

# --------------------

if flag == 0:
    print('0')
elif flag == 1:
    print('1')
elif flag == 2:
    print('2')
else:
    print('None')

不知道你的感受如何,笔者是觉得模式匹配语句容易理解得多,最起码你少写了很多个 flag == 吧(重复书写是好代码的敌人)。

如果有很多 if..elif..else 语句写在一坨,你需要小心去观察里面这些 elif/else 是否从属于同一个 if ;而 match 因为其缩进原因,逻辑块的从属关系会非常清晰。

此外, if 语句容易出错,比如漏写 else ,或者将 elif 写成 if 之类的沙雕操作;而模式匹配出这种低级错误的可能性小得多。

在很多语言中,模式匹配的执行效率更高,且容易维护。

如果不同的值需要进入相同的语句块,你可以用竖线 | 将其联合:

def http_error(status):
    match status:
        case 400:
            print("Bad request") 
        case 404:
            print("Not found") 
        case 418:
            print("I'm a teapot") 
        case 401 | 403:
            print("Not allowed") 
        case _:
            print("Something's wrong with the internet") 

http_error(418)
# I'm a teapot
http_error(403)
# Not allowed
http_error(999)
# Something's wrong with the internet

很简单吧,下面看点更高级的。

序列模式

如果你学过 C、Java 或 JavaScript 中的 switch 语句,那么则对 Python 的模式匹配并不陌生。不同的是, switch 语句常被用来将对象与 case 中的字面值进行比较;但 Python 的模式匹配可以处理更强大的结构化数据

比如,模式匹配可以处理元组

def foo(point):
    # point 是两个元素的序列
    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 _:
            print("Not a point")

foo((0, 0))
# Origin

foo((0, 99))
# Y=99

foo((99, 99))
# X=99, Y=99

foo((1, 2, 3))
# Not a point

foo(0)
# Not a point

元组中的变量 x, y 代表此处可以为任意值,并且此变量可以传递给 case 中的代码块。

注意:序列模式中,使用列表或者元组是没有差别的。

准确地说,要使得序列模式匹配成功,只要对象是 collections.abc.Sequence 的子类就可以(即为一个序列),哪怕这个对象不是内置的列表或元组。

看下面这个例子,它可以成功匹配 case (0, 0)

foo([0, 0])
# Origin

除了可以用变量 x, y 等变量来代表任意值外,case _ 同样可以用在结构化数据中:

match event:
    case ('warning', 500):
        print("A warning has been received.")
    case ('error', _):
        print(f"An error occurred.")

类模式

类(class)也可用于模式匹配。

看下面这个匹配坐标的例子:

class Point:
    x: int
    y: int

def location(event):
    match event:
        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")


p = Point()
(p.x, p.y) = (0, 9)

location(p)
# Y=9 and the point is on the y-axis.

位置参数

上面这种 Point(x=0, y=0) 的写法稍微有点繁琐。如果你想用位置参数初始化对象,可以用数据类 dataclass ,像这样:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def location(point):
    match point:
        case Point(0, 0):
            print("Origin is the point's location.")
        case Point(0, y):
            print(f"Y={y} and the point is on the y-axis.")
        case Point(x, 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")


p = Point(0, 9)
location(p)
# Y=9 and the point is on the y-axis.

注意仔细对比代码的变化。

嵌套

综合上面的例子,模式匹配能任意地嵌套,比如下面这种:

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.")

上面的代码,对空序列、单元素序列和多元素序列都做了妥善的处理。

这就是所谓的结构化数据模式匹配,是不是有点感觉了。

守护项

你可以在模式匹配中设置匹配条件。

比如 if 语句:

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.")

这个通常被称为守护项

对象混合

模式匹配甚至可以处理更加复杂的结构。

比如容器中具有不同的对象:

from dataclasses import dataclass

@dataclass
class Click:
    x: int
    y: int

@dataclass
class KeyPress:
    name: str

def action(event):
    match event:
        case Click(0, 0):
            print("Origin is the point's location.")
        case KeyPress('space'):
            print("Space key pressed.")
        case [Click(x, y), KeyPress('shift')]:
            print(f"Zoomming position at {x}, {y}")
        case _:
            print("Nothing happend.")

上面这个例子中 Click 表示用户点击了鼠标动作,KeyPress 表示按键动作。模式匹配可以将这两个不同的对象同时进行处理,这个看似很常用的操作,在某些语言中是办不到的。

枚举模式

模式匹配还可以处理枚举:

from enum import Enum
class Color(Enum):
    RED = 0
    GREEN = 1
    BLUE = 2

def get_color(color):
    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 :(")

映射模式

也可以处理字典:

persons = {'David': 19, 'Dusai': 21, 'Jack': 23}

match persons:
    case {'Dusai': 21, 'Jack': 23}:
        print('Dusai and Jack.')
    case {'David': _}:
        print('David.')

# 输出: Dusai and Jack.

对字典的处理要注意,模式匹配会捕获自己需要的键值,而额外的键值会被忽略。

总结

以上就是模式匹配的大部分基础使用场景了。

模式匹配比 if 语句更易阅读,通常执行效率也更高。

Python 中的模式匹配可以用于结构化数据,使得其应用场景(比某些编程语言中单纯比对字面量)更加广阔。

赶紧升级 Python 3.10,用它改造你的旧代码吧!

Python 3.10 更多细节的特性,推荐阅读 更新文档PEP634PEP635PEP636




本文作者: 杜赛
发布时间: 2021年10月22日 - 16:19
最后更新: 2021年10月22日 - 16:19
转载请保留原文链接及作者