Python 3.10 新特性:match-case语句的最佳案例
The following article is from Python开发精选 Author 南风草木香
【导语】:Python 3.10 版本中将引入结构化模式匹配,这一功能由match
语句来完成,该语句功能强大,可以轻松匹配字典,类以及其他更复杂的结构。
通过使用这种新的匹配方式能够简化代码,提高代码的可读性。
简介
本文将展示match
语句的最佳案例,探索如何使用它来编写Python代码。
colour = (25, 56, 200)
match colour:
case r, g, b:
print("No alpha.")
case r, g, b, alpha:
print(f"Alpha is {alpha}")
# Prints 'No alpha.'
(如果你刚打开网页,不知道 Pydon't 是什么,那么你需要查一下Pydon't介绍[1]。)
虽然match
语句看起来很像其他语言中的switch
语句,但并不仅仅如此。PEPs 634、635和636提供了大量信息介绍match
语句,包括:结构化模式匹配给Python带来了什么、如何使用它、以及它的基本原理等等。
项目地址:
https://www.python.org/dev/peps/pep-0636/
在本文中,我将主要介绍如何使用match
这个新特性来编写优雅的代码。在我撰写本文时,Python 3.10 仍然是一个预发布版本,所以如果你想使用 Python 3.10,可以看看这里[2]。
结构化模式匹配在 Python 中并不算一个新的概念。例如,我们可以使用 starred分配语句:
>>> a, *b, c = [1, 2, 3, 4, 5]
>>> a
1
>>> b
[2, 3, 4]
>>> c
5
此外,我们还可以使用深度解析:
>>> name, (r, g, b) = ("red", (250, 23, 10))
>>> name
'red'
>>> r
250
>>> g
23
>>> b
10
如果你不熟悉如何使用这些特性来编写python代码,可以查看Pydon'ts,我在“用starred分配语句解析”和“深度解析”中给出了详细说明。
match语句使用了starred分配语句和深度解析两者的思想,所以知道如何使用它们很重要。
简单使用
1.第一个match语句
让我们来实现阶乘函数。阶乘函数是介绍递归时的一个常用案例,你可以这样写:
def factorial(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial(n-1)
factorial(5) # 120
除了使用if语句之外,我们还可以尝试下使用match语句:
def factorial(n):
match n:
case 0 | 1:
return 1
case _:
return n * factorial(n - 1)
factorial(5)
注意以下几点:我们通过输入match n开始match语句,这意味着我们想根据n来做不同的事情;随后,我们用了两个case语句,这可以看作是需要处理的不同场景,每个case的后面是一个与n相对应的模式。
模式中也可以包含备选项,在case 0 | 1中由 | 区分,意味着无论n是0或1,都可以匹配。第二个模式中的case_是匹配所有东西的意思(当你不关心正在匹配的内容时),因此它的作用或多或少与第一个例子中的else相似。
2.模式匹配基本结构
正如上面所展示的那样,match语句可以当作简化版的if语句使用,此外,在处理结构化的数据时,match语句也有优势:
def normalise_colour_info(colour):
"""Normalise colour info to (name, (r, g, b, alpha))."""
match colour:
case (r, g, b):
name = ""
a = 0
case (r, g, b, a):
name = ""
case (name, (r, g, b)):
a = 0
case (name, (r, g, b, a)):
pass
case _:
raise ValueError("Unknown colour info.")
return (name, (r, g, b, a))
print(normalise_colour_info((240, 248, 255))) # ('', (240, 248, 255, 0))
print(normalise_colour_info((240, 248, 255, 0))) # ('', (240, 248, 255, 0))
print(normalise_colour_info(("AliceBlue", (240, 248, 255)))) # ('AliceBlue', (240, 248, 255, 0))
print(normalise_colour_info(("AliceBlue", (240, 248, 255, 0.3)))) # ('AliceBlue', (240, 248, 255, 0.3))
当颜色的结构匹配case中的情况时,则颜色的名称就变为该case中的变量名。这是对如下if语句版本的一个改进:
def normalise_colour_info(colour):
"""Normalise colour info to (name, (r, g, b, alpha))."""
if not isinstance(colour, (list, tuple)):
raise ValueError("Unknown colour info.")
if len(colour) == 3:
r, g, b = colour
name = ""
a = 0
elif len(colour) == 4:
r, g, b, a = colour
name = ""
elif len(colour) != 2:
raise ValueError("Unknown colour info.")
else:
name, values = colour
if not isinstance(values, (list, tuple)) or len(values) not in [3, 4]:
raise ValueError("Unknown colour info.")
elif len(values) == 3:
r, g, b = values
a = 0
else:
r, g, b, a = values
return (name, (r, g, b, a))
我尝试使用结构化模式匹配来完成语法简洁,功能相似的代码,但看起来效果并不好。
有人在评论中提出另一种不使用match语句的方案。该方案看起来比我的好,但比使用match语句相比,还是比较复杂繁多。
当需要给匹配版本中添加类型验证时,我们通过使用特定的值与Python的内置类型来进行匹配,match版本的效果显得更好:
def normalise_colour_info(colour):
"""Normalise colour info to (name, (r, g, b, alpha))."""
match colour:
case (int(r), int(g), int(b)):
name = ""
a = 0
case (int(r), int(g), int(b), int(a)):
name = ""
case (str(name), (int(r), int(g), int(b))):
a = 0
case (str(name), (int(r), int(g), int(b), int(a))):
pass
case _:
raise ValueError("Unknown colour info.")
return (name, (r, g, b, a)))
print(normalise_colour_info(("AliceBlue", (240, 248, 255)))) # ('AliceBlue', (240, 248, 255, 0))
print(normalise_colour_info2(("Red", (255, 0, "0")))) # ValueError: Unknown colour info.
你怎样用if语句来完成验证?
3.匹配对象结构
结构化匹配模式也可以用来匹配类的实例中的结构。让我们使用Point2D类来进行验证:
class Point2D:
"""A class to represent points in a 2D space."""
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""Provide a good-looking representation of the object."""
return f"({self.x}, {self.y})"
def __repr__(self):
"""Provide an unambiguous way of rebuilding this object."""
return f"Point2D({repr(self.x)}, {repr(self.y)})"
假设我们现在想编写一个函数,来取一个Point2D,并描述点的位置。我们可以使用模式匹配来提取x和y属性的值,而且我们还可以使用简短的if语句来缩小范围,达到成功匹配的目的!如下所示:
def describe_point(point):
"""Write a human-readable description of the point position."""
match point:
case Point2D(x=0, y=0):
desc = "at the origin"
case Point2D(x=0, y=y):
desc = f"in the vertical axis, at y = {y}"
case Point2D(x=x, y=0):
desc = f"in the horizontal axis, at x = {x}"
case Point2D(x=x, y=y) if x == y:
desc = f"along the x = y line, with x = y = {x}"
case Point2D(x=x, y=y) if x == -y:
desc = f"along the x = -y line, with x = {x} and y = {y}"
case Point2D(x=x, y=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(3, -3))) # The point is along the x = -y line, with x = 3 and y = -3
print(describe_point(Point2D(1, 2))) # The point is at (1, 2)
此外,我不知道上面代码片段中的所有x=和y=会让你感到烦恼吗?每次当我写一个新的模式point2D实例,我必须指出参数x和y是什么。对于类来说x和y是有顺序的,我们可以使用__match_args__来告诉Python我们想怎样匹配对象中的属性。
这里有一个小例子,用__match_args__来指定匹配Point2D时的先后顺序:
class Point2D:
"""A class to represent points in a 2D space."""
__match_args__ = ["x", "y"]
def __init__(self, x, y):
self.x = x
self.y = y
def describe_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):
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(1, 2))) # The point is at (1, 2)
4.通配符(单星号*)
在进行匹配时,还有一个好玩的东西-通配符。大多数情况下,你可以这样做:
>>> head, *body, tail = range(10)
>>> print(head, body, tail)
0 [1, 2, 3, 4, 5, 6, 7, 8] 9
此时,*body
告诉Python把所有head和tail之外的元素全部放进来,你还可以用 *
和列表及元组一起来匹配剩下的元素:
def rule_substitution(seq):
new_seq = []
while seq:
match seq:
case [x, y, z, *tail] if x == y == z:
new_seq.extend(["3", x])
case [x, y, *tail] if x == y:
new_seq.extend(["2", x])
case [x, *tail]:
new_seq.extend(["1", x])
seq = tail
return new_seq
seq = ["1"]
print(seq[0])
for _ in range(10):
seq = rule_substitution(seq)
print("".join(seq))
"""
Prints:
1
11
21
1211
111221
312211
13112221
1113213211
31131211131221
13211311123113112211
11131221133112132113212221
"""
运行结果就是上面的序列,通过观察上一行中的每个数字,并把它们描述出来,从而得到新的一行数字。例如,当你在一行中发现三个相等的数字,比如“222”时,你就可以把它重写为“32”,意思是出现了三个2。用match语句来实现上述功能非常容易。在上面的case语句中,用x、y和z来匹配序列的开头,用模式中的*tail匹配序列的剩余部分。
5.简单的字典匹配
类似地,我们可以使用**
来匹配字典的其余部分。但首先让我们看看如何匹配字典:
d = {0: "oi", 1: "uno"}
match d:
case {0: "oi"}:
print("yeah.")
# prints yeah.
字典d有一个键1,它的值为“uno”,我们输入match语句,此时在case语句中没有这个键值对。当匹配字典时,只匹配case中提到的结构,而不用管字典中的其他键。这与匹配列表或元组时的方法不同,如果没有提到通配符,则必须能全部匹配。
6.双星**
然而,如果你想知道原字典中没有匹配的键值对,你可以使用**
通配符:
d = {0: "oi", 1: "uno"}
match d:
case {0: "oi", **remainder}:
print(remainder)
# prints {1: 'uno'}
最后,如果你只想在字典中匹配指定的内容,可以这样做:
d = {0: "oi", 1: "uno"}
match d:
case {0: "oi", **remainder} if not remainder:
print("Single key in the dictionary")
case {0: "oi"}:
print("Has key 0 and extra stuff.")
# Has key 0 and extra stuff.
你还可以使用变量来匹配字典中键对应的值:
d = {0: "oi", 1: "uno"}
match d:
case {0: zero_val, 1: one_val}:
print(f"0 mapped to {zero_val} and 1 to {one_val}")
# 0 mapped to oi and 1 to uno
7.命名子模式
有时你可能想匹配一个更复杂的模式,随后为模式中的一部分或整体命名,方便后续引用。尤其是当你的模式出现替代选项的时候,通常用|来表示:
def go(direction):
match direction:
case "North" | "East" | "South" | "West":
return "Alright, I'm going!"
case _:
return "I can't go that way..."
print(go("North")) # Alright, I'm going!
print(go("asfasdf")) # I can't go that way...
现在,假设要匹配的模式嵌套在更复杂的结构中:
def act(command):
match command.split():
case "Cook", "breakfast":
return "I love breakfast."
case "Cook", *wtv:
return "Cooking..."
case "Go", "North" | "East" | "South" | "West":
return "Alright, I'm going!"
case "Go", *wtv:
return "I can't go that way..."
case _:
return "I can't do that..."
print("Go North") # Alright, I'm going!
print("Go asdfasdf") # I can't go that way...
print("Cook breakfast") # I love breakfast.
print("Drive") # I can't do that...
不仅如此,我们还想知道用户想去哪里,以便在消息中提示。我们可以用变量存储匹配的结果:
def act(command):
match command.split():
case "Cook", "breakfast":
return "I love breakfast."
case "Cook", *wtv:
return "Cooking..."
case "Go", "North" | "East" | "South" | "West" as direction:
return f"Alright, I'm going {direction}!"
case "Go", *wtv:
return "I can't go that way..."
case _:
return "I can't do that..."
print("Go North") # Alright, I'm going North!
print("Go asdfasdf") # I can't go that way...
8.处理递归结构
结构模式匹配有望取得成功的另一个优点是处理递归结构。我看到了许多很好的例子,下面分享下我的方案。假设你想要将一个数学表达式转换为前缀表示法,例如,“3 * 4”变成了“* 3 4”,1 + 2 + 3变成了+ 1 + 2 3或+ + 1 2 3,这取决于+从左边还是从右边联系整个等式。你可以用match来解决这个问题:
import ast
def prefix(tree):
match tree:
case ast.Expression(expr):
return prefix(expr)
case ast.Constant(value=v):
return str(v)
case ast.BinOp(lhs, op, rhs):
match op:
case ast.Add():
sop = "+"
case ast.Sub():
sop = "-"
case ast.Mult():
sop = "*"
case ast.Div():
sop = "/"
case _:
raise NotImplementedError()
return f"{sop} {prefix(lhs)} {prefix(rhs)}"
case _:
raise NotImplementedError()
print(prefix(ast.parse("1 + 2 + 3", mode="eval"))) # + + 1 2 3
print(prefix(ast.parse("2**3 + 6", mode="eval")) # + * 2 3 6
print(prefix(ast.parse("1 + 2*3 - 5/7", mode="eval"))) # - + 1 * 2 3 / 5 7
9.谨慎面对炒作
需要注意的是:match并不一定适合所有情况。看看上面的前缀表示法的例子,也许有更好的方法将每个二进制操作符转换为字符串形式表示出来?当前解决方案针对每个不同的运算符写了两行代码,如果我们对更多二进制操作符提供支持,就可以省去许多冗余的代码。我们可以这样做:
import ast
def op_to_str(op):
ops = {
ast.Add: "+",
ast.Sub: "-",
ast.Mult: "*",
ast.Div: "/",
}
return ops.get(op.__class__, None)
def prefix(tree):
match tree:
case ast.Expression(expr):
return prefix(expr)
case ast.Constant(value=v):
return str(v)
case ast.BinOp(lhs, op, rhs):
sop = op_to_str(op)
if sop is None:
raise NotImplementedError()
return f"{sop} {prefix(lhs)} {prefix(rhs)}"
case _:
raise NotImplementedError()
print(prefix(ast.parse("1 + 2 + 3", mode="eval"))) # + + 1 2 3
print(prefix(ast.parse("2*3 + 6", mode="eval")) # + * 2 3 6
print(prefix(ast.parse("1 + 2*3 - 5/7", mode="eval"))) # - + 1 * 2 3 / 5 7
结论
下面是这篇文章的主要内容:“结构化模式匹配用于Python,可以减少代码量,增加可读性,但并不适合在所有情况下都使用它”。
这篇Pydon't讲述了这些内容:
使用 match
语句来进行结构化模式匹配,为已有的starred分配语句和结构化分配增加了新特性结构化模式匹配可以匹配字符值和任意模式 在模式中可以用 if
语句来处理附加条件模式中可以使用通配符 *
和**
match
语句在处理类的实例这种结构时,具有非常强大的功能当在 case
中使用自定义类时,可以用__match_args__
为参数指定匹配顺序可以在 case
中使用Python内置类来验证类型
参考资料
Pydon't介绍: https://mathspp.com/blog/pydonts/pydont-manifesto
[2]可以看看这里: https://www.python.org/download/pre-releases/
- EOF -
1、太酷炫了,我用 Python 画出了北上广深的地铁路线动态图
觉得本文对你有帮助?请分享给更多人
推荐关注「Python开发者」,提升Python技能
点赞和在看就是最大的支持❤️