美文网首页
Python 3.10 新特性 —— 结构化模式匹配(Struc

Python 3.10 新特性 —— 结构化模式匹配(Struc

作者: rollingstarky | 来源:发表于2022-04-02 20:30 被阅读0次

    switch-case

    众所周知,Python 中是没有类似 switch-case 结构的语法的。但是自从 3.10 版本发布以后,这种说法就已经成为历史了。

    Java 中的 switch 语句类似如下形式:

    public class Main {
            public static void main(String[] args) {
                    var option = 3;
    
                    switch (option) {
                            case 1:
                                    System.out.println("You have chosen option 1.");
                                    break;
                            case 2:
                                    System.out.println("You have chosen option 2.");
                                    break;
                            case 3:
                                    System.out.println("You have chosen option 3.");
                                    break;
                            default:
                                    System.out.println("Sorry you chose an invalid option.");
                                    break;
                    }
            }
    }
    

    看起来就像是另一种形式的 if-else 语句。以某个变量值作为判断条件,根据不同的判断结果执行对应的语句,最终形成一种流程上的分支结构。
    这也许是 Python 不去实现它的依据(借口)之一?都已经有了 if-else 可以足够轻松地完成同样的事情。

    There should be one-- and preferably only one --obvious way to do it.

    字典映射

    在模式匹配出现之前,对于分支相当多的判断语句,Python 建议通过字典映射(dictionary mapping)来实现。

    def function_map(option):
         return {
                1: lambda : print('You have chose option 1.'),
                2: lambda : print('You have chose option 2.'),
                3: lambda : print('You have chose option 3.')
                }.get(option, lambda: print('Sorry you chose an invalid option.'))
    
    function_map(3)()
    

    借助字典这种数据结构,以匹配条件作为键值,一一对应匹配后需要执行的命令。将 switch 结构中的条件判断转化为对字典键值的搜索匹配。

    Pattern Match

    用模式匹配实现 switch-case 语法,从形式上看就直观了很多:

    option = 3
    
    match option:
        case 1:
            print("You have chosen option 1.")
        case 2:
            print("You have chosen option 2.")
        case 3:
            print("You have chosen option 3.")
        case _:
            print("You chose an invalid option.")
    

    实际上模式匹配不只有创建流程上的分支结构这一种功能,它的作用可以比单纯的 switch-case 语法强大的多。

    模式匹配可以算是一种历史悠久的编程技巧了,经常可以在函数式编程语言中见到。比较有代表性的语言比如 Haskell。相对年轻的语言比如 Rust 也引入了功能强大的模式匹配语法。
    模式匹配其实可以拆成两部分来理解:匹配和模式。
    匹配部分可以发挥类似于 if-elseswitch 等条件判断语句的作用,生成一种分支结构;模式则定义了特定的规则即匹配的具体条件。更进一步的,还会对匹配到的对象进行解构(destructuring)或者说拆包(unpacking)。

    以不同于模式匹配的正则表达式来说:

    import re
    
    source_str = 'cats are cute'
    pattern = re.compile('(.*) are (.*)')
    
    matched = re.match(pattern, source_str)
    print(matched.groups())
    # => ('cats', 'cute')
    

    正则表达式规则中的 (.*) 分别匹配到源字符串中的 catscute,与此同时,还把这两个匹配项提取了出来。

    而模式匹配相对来说,则不仅仅能够匹配和提取 catscute 等字符串类型,还能够匹配更复杂类型的对象,同时对匹配到的对象进行拆包操作。

    比如下面的代码就对类型为元组的对象进行了匹配和拆包:

    def match_person(person):
        match person:
            case (name, 'M', age):
                print(f'He is {name}, aged {age}.')
            case (name, 'F', age):
                print(f'She is {name}, aged {age}.')
            case (name,):
                print(f'We only know the name is {name}, others are secrets.')
    
    person_A = ('John', 'M', 20)
    person_B = ('Jenny', 'F', 18)
    person_C = ('Lily',)
    
    match_person(person_A)
    # => He is John, aged 20.
    match_person(person_B)
    # => She is Jenny, aged 18.
    match_person(person_C)
    # => We only know the name is Lily, others are secrets.
    

    match 关键字后面被匹配的对象,支持很多种复杂的类型。对应的 case 关键字后面的模式也同样灵活:

    • 列表或元组,如 (name, 18)
    • 字典,如 {"name": name, "age": 18}
    • 使用 * 匹配列表中的剩余部分,如 [first, *rest]
    • 使用 ** 匹配字典中的剩余部分
    • 匹配对象和对象的属性
    • 在模式中可以使用 | 逻辑或操作

    模式匹配应用实例

    创建一个 Python 程序,模拟交互式命令行的行为。

    匹配字符串
    def run_command(command: str) -> None:
        match command:
            case "quit":
                print("Quitting the program.")
                quit()
            case "reset":
                print("Resetting the system.")
            case other:
                print(f"Unknown command: {other!r}.")
    
    def main() -> None:
        while True:
            command = input("$ ")
            run_command(command)
    
    
    if __name__ == '__main__':
        main()
    

    运行效果如下:

    $ reset
    Resetting the system.
    $ abcdefg
    Unknown command: 'abcdefg'.
    $ quit
    Quitting the program.
    
    匹配列表
    def run_command(command: str):
        match command.split():
            case ["load", filename]:
                print(f"Loading file: {filename}.")
            case ["save", filename]:
                print(f"Saving to file: {filename}.")
            case ["quit" | "exit" | "bye"]:
                print("Quitting the program.")
                quit()
            case _:
                print(f"Unkown command: {command!r}.")
    
    
    def main() -> None:
        while True:
            command = input("$ ")
            run_command(command)
    
    
    if __name__ == '__main__':
        main()
    

    运行效果:

    $ load input_data.txt
    Loading file: input_data.txt.
    $ save output_data.txt
    Saving to file: output_data.txt.
    $ load input_data.txt output_data.txt
    Unkown command: 'load input_data.txt output_data.txt'.
    $ bye
    Quitting the program.
    
    匹配对象
    from dataclasses import dataclass
    from typing import List
    import shlex
    
    
    @dataclass
    class Command:
        command: str
        arguments: List[str]
    
    
    def run_command(command: Command):
        match command:
            case Command(command="load", arguments=[filename]):
                print(f"Loading file: {filename}.")
            case Command(command="save", arguments=[filename]):
                print(f"Saving to file: {filename}.")
            case Command(command="quit" | "exit" | "bye", arguments=["--force" | "-f"]):
                print("Sending SIGTERM and quitting the program.")
                quit()
            case Command(command="quit" | "exit" | "bye"):
                print("Quitting the program.")
                quit()
            case _:
                print(f"Unknown command: {command!r}.")
    
    
    def main() -> None:
        while True:
            command, *arguments = shlex.split(input("$ "))
            run_command(Command(command, arguments))
    
    
    if __name__ == '__main__':
        main()
    

    运行效果:

    $ not a command
    Unknown command: Command(command='not', arguments=['a', 'command']).
    $ load input_data.txt
    Loading file: input_data.txt.
    $ save output_data.txt
    Saving to file: output_data.txt.
    $ exit -f
    Sending SIGTERM and quitting the program.
    

    需要注意的是,模式匹配中各条 case 语句之间的前后顺序是至关重要的。通常来说,更“具体”更“精确”一些的规则要放在相对靠前的位置。
    假如 case Command(command="quit" | "exit" | "bye") 为规则 1,Command(command="quit" | "exit" | "bye", arguments=["--force" | "-f"]) 为规则 2。
    则更具体一些的规则 2 要放在规则 1 前面。因为模式匹配是从上到下依次检查每一个 case 语句,若遇到匹配的模式,则执行对应的命令。不再继续向下匹配。
    由于严格符合规则 2 的对象一定也符合规则 1,当规则 1 位于规则 2 前面时,规则 2 永远也没有被匹配的机会。

    可以想象成一种逐渐“滑落”的过程。比如写一个计算成绩等级的函数,可以这样实现:

    def grade(score):
        if score >= 90:
            return 'A'
        elif score >= 70:
            return 'B'
        elif score >= 60:
            return 'C'
    

    如果上面 if-else 的条件反着排,那就,所有人都是 C 了。。。

    参考资料

    A Closer Look At Structural Pattern Matching // New In Python 3.10!

    相关文章

      网友评论

          本文标题:Python 3.10 新特性 —— 结构化模式匹配(Struc

          本文链接:https://www.haomeiwen.com/subject/yiqajrtx.html