查看原文
其他

Python 3.10 新特性:match-case语句的最佳案例

The following article is from Python开发精选 Author 南风草木香

【导语】:Python 3.10 版本中将引入结构化模式匹配,这一功能由match语句来完成,该语句功能强大,可以轻松匹配字典,类以及其他更复杂的结构。

通过使用这种新的匹配方式能够简化代码,提高代码的可读性。

简介

本文将展示match语句的最佳案例,探索如何使用它来编写Python代码。

colour = (2556200)

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 = [12345]
>>> a
1
>>> b
[234]
>>> c
5

此外,我们还可以使用深度解析:

>>> name, (r, g, b) = ("red", (2502310))
>>> 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((240248255)))                       # ('', (240, 248, 255, 0))
print(normalise_colour_info((2402482550)))                    # ('', (240, 248, 255, 0))
print(normalise_colour_info(("AliceBlue", (240248255))))        # ('AliceBlue', (240, 248, 255, 0))
print(normalise_colour_info(("AliceBlue", (2402482550.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 [34]:
            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", (240248255))))    # ('AliceBlue', (240, 248, 255, 0))
 print(normalise_colour_info2(("Red", (2550"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(00)))    # The point is at the origin
     print(describe_point(Point2D(30)))    # 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(12)))    # 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(00):
            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(00)))    # The point is at the origin
print(describe_point(Point2D(30)))    # The point is in the horizontal axis, at x = 3
print(describe_point(Point2D(12)))    # The point is at (1, 2)

4.通配符(单星号*)

在进行匹配时,还有一个好玩的东西-通配符。大多数情况下,你可以这样做:

>>> head, *body, tail = range(10)
>>> print(head, body, tail)
0 [123456789

此时,*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内置类来验证类型

参考资料

[1]

Pydon't介绍: https://mathspp.com/blog/pydonts/pydont-manifesto

[2]

可以看看这里: https://www.python.org/download/pre-releases/



- EOF -

推荐阅读  点击标题可跳转

1、太酷炫了,我用 Python 画出了北上广深的地铁路线动态图

2、如何使用 python 提取 PDF 表格及文本,并保存到 Excel

3、Python 怎么捕获警告?(注意:不是捕获异常)


觉得本文对你有帮助?请分享给更多人

推荐关注「Python开发者」,提升Python技能

点赞和在看就是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存