Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 636 – 结构化模式匹配:教程

作者:
Daniel F Moisset <dfmoisset at gmail.com>
赞助人:
Guido van Rossum <guido at python.org>
BDFL 代表:

讨论邮件列表:
Python-Dev 列表
状态:
最终
类型:
信息性
创建日期:
2020 年 9 月 12 日
Python 版本:
3.10
历史记录:
2020 年 10 月 22 日,2021 年 2 月 8 日
决议:
Python 提交者邮件

目录

摘要

本 PEP 是 PEP 634 引入的模式匹配的教程。

PEP 622 提出了一种模式匹配语法,并获得了社区和指导委员会的详细讨论。一个常见的担忧是这个特性是否容易解释(和学习)。本 PEP 通过提供开发人员可以用来学习 Python 中模式匹配的文档来解决此问题。

这被认为是 PEP 634(模式匹配的技术规范)和 PEP 635(模式匹配的动机和基本原理以及设计考虑因素)的辅助材料。

对于那些希望快速回顾而不是学习教程的读者,请参阅 附录 A

教程

为了激励本教程,您将编写一个文字冒险游戏。这是一种交互式小说的形式,用户输入文本命令与虚构世界交互,并收到对发生情况的文本描述。命令将是自然语言的简化形式,例如 get swordattack dragongo northenter shopbuy cheese

匹配序列

您的主循环需要从用户那里获取输入并将其拆分为单词,假设是一个像这样的字符串列表

command = input("What are you doing next? ")
# analyze the result of command.split()

下一步是解释这些单词。我们的大多数命令都将有两个单词:一个动作和一个对象。因此,您可能倾向于执行以下操作

[action, obj] = command.split()
... # interpret action, obj

该代码行的问题在于它缺少了一些东西:如果用户输入的单词多于或少于 2 个怎么办?为了防止此问题,您可以检查单词列表的长度,或捕获上面语句将引发的 ValueError

您可以改用匹配语句

match command.split():
    case [action, obj]:
        ... # interpret action, obj

match 语句评估“主体”match 关键字之后的 value),并根据模式case 旁边的代码)进行检查。模式能够做两件事

  • 验证主体是否具有特定的结构。在您的例子中,[action, obj] 模式匹配任何正好包含两个元素的序列。这称为匹配
  • 它将模式中的一些名称绑定到主题的组件元素。在这种情况下,如果列表有两个元素,它将绑定 action = subject[0]obj = subject[1]

如果匹配,则 case 块内的语句将使用绑定的变量执行。如果没有匹配,则不会发生任何事情,并且 match 之后的语句将被接下来执行。

请注意,与解包赋值类似,您可以使用括号、方括号或逗号分隔作为同义词。因此,您可以使用 case action, objcase (action, obj),含义相同。所有形式都将匹配任何序列(例如列表或元组)。

匹配多个模式

即使大多数命令都具有 action/object 形式,您可能也希望用户命令具有不同的长度。例如,您可能希望添加没有对象的单一动词,例如 lookquit。一个 match 语句可以(并且很可能)有多个 case

match command.split():
    case [action]:
        ... # interpret single-verb action
    case [action, obj]:
        ... # interpret action, obj

match 语句将从上到下检查模式。如果模式与主体不匹配,则将尝试下一个模式。但是,一旦找到第一个匹配模式,则将执行该 case 的主体,并且所有后续 case 都将被忽略。这类似于 if/elif/elif/... 语句的工作方式。

匹配特定值

您的代码仍然需要查看特定的动作,并根据特定的动作(例如 quitattackbuy)有条件地执行不同的逻辑。您可以使用 if/elif/elif/... 链或函数字典来执行此操作,但在这里我们将利用模式匹配来解决此任务。您可以使用模式中的字面值(如 "quit"42None)而不是变量。这允许您编写

match command.split():
    case ["quit"]:
        print("Goodbye!")
        quit_game()
    case ["look"]:
        current_room.describe()
    case ["get", obj]:
        character.get(obj, current_room)
    case ["go", direction]:
        current_room = current_room.neighbor(direction)
    # The rest of your commands go here

["get", obj] 这样的模式将仅匹配第一个元素等于 "get" 的 2 元素序列。它还将绑定 obj = subject[1]

如您在 go 案例中看到的,我们还可以在不同的模式中使用不同的变量名。

字面值与 == 运算符进行比较,但常量 TrueFalseNone 除外,它们与 is 运算符进行比较。

匹配多个值

玩家可以通过使用一系列命令 drop keydrop sworddrop cheese 来丢弃多个物品。此界面可能很麻烦,您可能希望允许在单个命令中丢弃多个物品,例如 drop key sword cheese。在这种情况下,您事先不知道命令中有多少个单词,但您可以像在赋值中允许的那样在模式中使用扩展解包

match command.split():
    case ["drop", *objects]:
        for obj in objects:
            character.drop(obj, current_room)
    # The rest of your commands go here

这将匹配任何以“drop”作为其第一个元素的序列。所有剩余的元素都将被捕获到一个 list 对象中,该对象将绑定到 objects 变量。

此语法具有与序列解包类似的限制:您在一个模式中不能有多个星号名称。

添加通配符

当所有模式都失败时,您可能希望打印一条错误消息,说明未识别命令。您可以使用我们刚刚学习的功能并编写 case [*ignored_words] 作为您的最后一个模式。但是,有一种更简单的方法

match command.split():
    case ["quit"]: ... # Code omitted for brevity
    case ["go", direction]: ...
    case ["drop", *objects]: ...
    ... # Other cases
    case _:
        print(f"Sorry, I couldn't understand {command!r}")

此特殊模式写为 _(称为通配符),始终匹配,但它不绑定任何变量。

请注意,这将匹配任何对象,而不仅仅是序列。因此,只有将其单独用作最后一个模式才有意义(为了防止错误,Python 将阻止您在之前使用它)。

组合模式

现在是时候从示例中退一步,了解您一直在使用的模式是如何构建的了。模式可以相互嵌套,我们在上面的示例中已经隐式地做到了这一点。

我们已经看到了一些“简单”模式(这里的“简单”是指它们不包含其他模式)

  • 捕获模式(独立名称,如 directionactionobjects)。我们从未单独讨论过这些,而是将它们用作其他模式的一部分。
  • 字面量模式(字符串字面量、数字字面量、TrueFalseNone
  • 通配符模式 _

到目前为止,我们尝试的唯一非简单模式是序列模式。序列模式中的每个元素实际上可以是任何其他模式。这意味着你可以编写一个像 ["first", (left, right), _, *rest] 这样的模式。这将匹配至少包含三个元素的序列主题,其中第一个元素等于 "first",第二个元素依次是一个包含两个元素的序列。它还将绑定 left=subject[1][0]right=subject[1][1]rest = subject[3:]

或模式

回到冒险游戏示例,你可能会发现你希望几个模式产生相同的结果。例如,你可能希望命令 northgo north 等价。你可能也希望为 get Xpick up Xpick X up(对于任何 X)设置别名。

模式中的 | 符号将它们组合成备选方案。例如,你可以编写

match command.split():
    ... # Other cases
    case ["north"] | ["go", "north"]:
        current_room = current_room.neighbor("north")
    case ["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"]:
        ... # Code for picking up the given object

这称为**或模式**,并将产生预期结果。模式从左到右尝试;如果多个备选方案匹配,则了解绑定内容可能与之相关。编写或模式时的一个重要限制是所有备选方案都应绑定相同的变量。因此,模式 [1, x] | [2, y] 不允许,因为它会使成功匹配后绑定哪个变量不清楚。[1, x] | [2, x] 完美且始终会在成功时绑定 x

捕获匹配的子模式

我们“go”命令的第一个版本是用 ["go", direction] 模式编写的。我们在上一个版本中使用模式 ["north"] | ["go", "north"] 所做的更改有一些好处,但也有一些缺点:最新版本允许使用别名,但也将方向硬编码,这将迫使我们为 north/south/east/west 实际使用单独的模式。这会导致一些代码重复,但同时我们获得了更好的输入验证,并且如果用户输入的命令是 "go figure!" 而不是方向,我们将不会进入该分支。

我们可以尝试做到两全其美(为了简洁起见,我将省略没有“go”的别名版本)

match command.split():
    case ["go", ("north" | "south" | "east" | "west")]:
        current_room = current_room.neighbor(...)
        # how do I know which direction to go?

此代码是一个分支,它验证“go”后面的单词是否真的是方向。但是,移动玩家的代码需要知道选择了哪一个,并且无法做到这一点。我们需要一个像或模式一样工作但同时进行捕获的模式。我们可以使用**as 模式**做到这一点

match command.split():
    case ["go", ("north" | "south" | "east" | "west") as direction]:
        current_room = current_room.neighbor(direction)

as 模式匹配其左侧的任何模式,但也将值绑定到一个名称。

为模式添加条件

我们上面探讨的模式可以进行一些强大的数据过滤,但有时你可能希望使用布尔表达式的全部功能。假设你实际上只想在基于当前房间可能出口的受限方向集中允许“go”命令。我们可以通过在我们的 case 中添加**守卫**来实现这一点。守卫由 if 关键字后跟任何表达式组成

match command.split():
    case ["go", direction] if direction in current_room.exits:
        current_room = current_room.neighbor(direction)
    case ["go", _]:
        print("Sorry, you can't go that way")

守卫不是模式的一部分,它是 case 的一部分。只有在模式匹配并且所有模式变量都已绑定后才会检查它(这就是为什么条件可以在上面的示例中使用 direction 变量的原因)。如果模式匹配且条件为真,则 case 的主体正常执行。如果模式匹配但条件为假,则匹配语句继续检查下一个 case,就好像模式没有匹配一样(可能存在已绑定某些变量的副作用)。

添加 UI:匹配对象

你的冒险正在取得成功,你被要求实现一个图形界面。你选择的 UI 工具包允许你编写一个事件循环,你可以在其中通过调用 event.get() 获取新的事件对象。根据用户操作,生成的 object 可以具有不同的类型和属性,例如

  • 当用户按下键时,会生成一个 KeyPress 对象。它有一个 key_name 属性,其中包含所按键的名称,以及一些关于修饰符的其他属性。
  • 当用户单击鼠标时,会生成一个 Click 对象。它有一个 position 属性,其中包含指针的坐标。
  • 当用户单击游戏窗口的关闭按钮时,会生成一个 Quit 对象。

与其编写多个 isinstance() 检查,你可以使用模式来识别不同类型的对象,并将其应用于其属性

match event.get():
    case Click(position=(x, y)):
        handle_click_at(x, y)
    case KeyPress(key_name="Q") | Quit():
        game.quit()
    case KeyPress(key_name="up arrow"):
        game.go_north()
    ...
    case KeyPress():
        pass # Ignore other keystrokes
    case other_event:
        raise ValueError(f"Unrecognized event: {other_event}")

Click(position=(x, y)) 这样的模式仅在事件类型是 Click 类的子类时才匹配。它还需要事件具有一个 position 属性,该属性与 (x, y) 模式匹配。如果匹配,则局部变量 xy 将获得预期值。

KeyPress() 这样的模式,没有参数,将匹配任何是 KeyPress 类实例的对象。仅匹配你在模式中指定的属性,而忽略任何其他属性。

匹配位置属性

上一节描述了在进行对象匹配时如何匹配命名属性。对于某些对象,通过位置描述匹配的参数可能很方便(尤其是在只有几个属性并且它们具有“标准”顺序时)。如果正在使用的类是命名元组或数据类,则可以通过按照构建对象时使用的相同顺序来执行此操作。例如,如果上面的 UI 框架这样定义它们的类

from dataclasses import dataclass

@dataclass
class Click:
    position: tuple
    button: Button

那么你可以将上面的匹配语句重写为

match event.get():
    case Click((x, y)):
        handle_click_at(x, y)

(x, y) 模式将自动与 position 属性匹配,因为模式中的第一个参数对应于数据类定义中的第一个属性。

其他类没有其属性的自然顺序,因此你必须在模式中使用显式名称来与其属性匹配。但是,可以手动指定属性的顺序以允许位置匹配,就像在此备用定义中一样

class Click:
    __match_args__ = ("position", "button")
    def __init__(self, pos, btn):
        self.position = pos
        self.button = btn
        ...

__match_args__ 特殊属性为你的属性定义了一个显式顺序,可以在像 case Click((x,y)) 这样的模式中使用。

匹配常量和枚举

你上面的模式将所有鼠标按钮视为相同,并且你已决定要接受左键单击,并忽略其他按钮。在执行此操作时,你注意到 button 属性的类型为 Button,它是一个使用 enum.Enum 构建的枚举。实际上,你可以像这样匹配枚举值

match event.get():
    case Click((x, y), button=Button.LEFT):  # This is a left click
        handle_click_at(x, y)
    case Click():
        pass  # ignore other clicks

这将适用于任何带点名称(如 math.pi)。但是,未限定的名称(即没有点的裸名称)将始终被解释为捕获模式,因此请避免这种歧义,在模式中始终使用限定常量。

进入云端:映射

你已决定制作游戏的在线版本。你的所有逻辑都将在服务器中,而 UI 在客户端中,它们将使用 JSON 消息进行通信。通过 json 模块,这些将映射到 Python 字典、列表和其他内置对象。

我们的客户端将接收一个字典列表(从 JSON 解析),用于执行操作,每个元素例如如下所示

  • {"text": "The shop keeper says 'Ah! We have Camembert, yes sir'", "color": "blue"}
  • 如果客户端应该暂停 {"sleep": 3}
  • 播放声音 {"sound": "filename.ogg", "format": "ogg"}

到目前为止,我们的模式已处理序列,但有模式可以根据其存在的键匹配映射。在这种情况下,你可以使用

for action in actions:
    match action:
        case {"text": message, "color": c}:
            ui.set_text_color(c)
            ui.display(message)
        case {"sleep": duration}:
            ui.wait(duration)
        case {"sound": url, "format": "ogg"}:
            ui.play(url)
        case {"sound": _, "format": _}:
            warning("Unsupported audio format")

映射模式中的键需要是文字,但值可以是任何模式。与序列模式一样,所有子模式都必须匹配才能使通用模式匹配。

你可以在映射模式中使用 **rest 来捕获主题中的其他键。请注意,如果省略此项,则在匹配时将忽略主题中的额外键,即消息 {"text": "foo", "color": "red", "style": "bold"} 将匹配上面示例中的第一个模式。

匹配内置类

上面的代码可以使用一些验证。鉴于消息来自外部来源,字段的类型可能错误,从而导致错误或安全问题。

任何类都是有效的匹配目标,包括内置类,如bool strint。这使我们能够将上述代码与类模式结合起来。因此,与其编写 {"text": message, "color": c},我们可以使用 {"text": str() as message, "color": str() as c} 来确保 messagec 都是字符串。对于许多内置类(有关完整列表,请参见PEP 634),您可以使用位置参数作为简写,编写 str(c) 而不是 str() as c。完全重写的版本如下所示

for action in actions:
    match action:
        case {"text": str(message), "color": str(c)}:
            ui.set_text_color(c)
            ui.display(message)
        case {"sleep": float(duration)}:
            ui.wait(duration)
        case {"sound": str(url), "format": "ogg"}:
            ui.play(url)
        case {"sound": _, "format": _}:
            warning("Unsupported audio format")

附录 A – 快速入门

匹配语句获取一个表达式,并将其值与作为一个或多个 case 块给出的后续模式进行比较。这在表面上类似于 C、Java 或 JavaScript(以及许多其他语言)中的 switch 语句,但功能更强大。

最简单的形式将主题值与一个或多个字面量进行比较

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"

请注意最后一个块:“变量名”_ 充当通配符,并且永远不会匹配失败。

您可以使用 |(“或”)将多个字面量组合到单个模式中

case 401 | 403 | 404:
    return "Not allowed"

模式看起来像解包赋值,可用于绑定变量

# point is an (x, y) tuple
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 _:
        raise ValueError("Not a point")

仔细研究一下!第一个模式有两个字面量,可以认为是上面显示的字面量模式的扩展。但接下来的两个模式将字面量和变量组合在一起,变量绑定来自主题(point)的值。第四个模式捕获两个值,这使得它在概念上类似于解包赋值 (x, y) = point

如果您使用类来构建数据,则可以使用类名称后跟类似于构造函数的参数列表,但能够将属性捕获到变量中

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

您可以对某些提供其属性排序的内置类(例如 dataclasses)使用位置参数。您还可以通过在类中设置 __match_args__ 特殊属性来定义属性的特定位置。如果将其设置为(“x”,“y”),则以下模式都等效(并且都将 y 属性绑定到 var 变量)

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

模式可以任意嵌套。例如,如果我们有一个点的短列表,我们可以这样匹配它

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

我们可以向模式添加一个 if 子句,称为“守卫”。如果守卫为假,match 将继续尝试下一个 case 块。请注意,值捕获发生在评估守卫之前

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

其他几个关键特性

  • 与解包赋值一样,元组和列表模式具有完全相同的含义,实际上匹配任意序列。一个重要的例外是它们不匹配迭代器或字符串。(从技术上讲,主题必须是 collections.abc.Sequence 的实例。)
  • 序列模式支持通配符:[x, y, *rest](x, y, *rest) 的工作原理类似于解包赋值中的通配符。* 后的名称也可能是 _,因此 (x, y, *_) 匹配至少两个项目的序列,而不会绑定剩余的项目。
  • 映射模式:{"bandwidth": b, "latency": l} 从字典中捕获 "bandwidth""latency" 值。与序列模式不同,额外的键将被忽略。还支持通配符 **rest。(但 **_ 将是多余的,因此不允许。)
  • 可以使用 as 关键字捕获子模式
    case (Point(x1, y1), Point(x2, y2) as p2): ...
    
  • 大多数字面量通过相等性进行比较,但是单例 TrueFalseNone 通过同一性进行比较。
  • 模式可以使用命名常量。这些必须是点分名称,以防止将其解释为捕获变量
    from enum import Enum
    class Color(Enum):
        RED = 0
        GREEN = 1
        BLUE = 2
    
    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 :(")
    

来源:https://github.com/python/peps/blob/main/peps/pep-0636.rst

上次修改时间:2023-09-09 17:39:29 GMT