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

Python 增强提案

PEP 622 – 结构化模式匹配

作者:
Brandt Bucher <brandt at python.org>, Daniel F Moisset <dfmoisset at gmail.com>, Tobias Kohn <kohnt at tobiaskohn.ch>, Ivan Levkivskyi <levkivskyi at gmail.com>, Guido van Rossum <guido at python.org>, Talin <viridia at gmail.com>
BDFL 委托

讨论至:
Python-Dev 列表
状态:
已取代
类型:
标准跟踪
创建日期:
2020 年 6 月 23 日
Python 版本:
3.10
发布历史:
2020 年 6 月 23 日, 2020 年 7 月 8 日
取代者:
634

目录

摘要

本 PEP 提议向 Python 添加一个 **模式匹配语句**,其灵感来自 Scala、Erlang 等语言中的类似语法。

模式和形状

**模式语法** 构建在 Python 现有的序列解包语法之上(例如,a, b = value)。

match 语句将一个值(**主体**)与几个不同的形状(**模式**)进行比较,直到找到一个匹配的形状。每个模式描述了可接受值​​的类型和结构,以及用于捕获其内容的变量。

模式可以指定形状为

  • 如前所述,一个要解包的序列
  • 一个具有特定键的映射
  • 一个具有(可选)特定属性的给定类的实例
  • 一个特定的值
  • 一个通配符

模式可以以多种方式组合。

语法

从语法上讲,match 语句包含

  • 一个主体表达式
  • 一个或多个 case 子句

每个 case 子句指定

  • 一个模式(要匹配的整体形状)
  • 一个可选的“守卫”(如果模式匹配则检查的条件)
  • 如果选择该 case 子句,则要执行的代码块

动机

PEP 的其余部分

  • 阐述了为什么我们认为模式匹配是 Python 的一个好补充
  • 解释了我们的设计选择
  • 包含精确的语法和运行时规范
  • 为静态类型检查器提供了指导(并对 typing 模块进行了一些小的补充)
  • 讨论了在提案经过广泛讨论(包括作者小组和 python-dev 社区)期间提出的主要反对意见和替代方案

最后,我们讨论了一些可能的扩展,一旦社区对当前提议的语法和语义有了充分的经验,就可以在未来考虑这些扩展。

概述

模式是一种新的语法类别,有其自己的规则和特殊情况。模式以新颖的方式混合了输入(给定值)和输出(捕获的变量)。它们可能需要一些时间才能有效使用。作者在此提供了一个基本概念的简要介绍。请注意,本节无意完全详尽或完全准确。

模式,一种新的语法结构,以及解构

本 PEP 引入了一种称为**模式**的新语法结构。从语法上看,模式看起来像是表达式的一个子集。以下是模式的示例

  • [first, second, *rest]
  • Point2d(x, 0)
  • {"name": "Bruce", "age": age}
  • 42

上面表达式看起来可能像是对象构造的示例,其中构造函数接受一些值作为参数并从这些组件构建对象。

当作为模式查看时,上述模式意味着构造的反向操作,我们称之为**解构**。**解构** 获取一个主体值并提取其组件。

对象构造和解构之间的语法相似性是故意的。它也遵循 Python 中上下文的现有风格,即赋值目标(写上下文)看起来像表达式(读上下文)。

模式匹配从不创建对象。这就像 [a, b] = my_list 不会创建一个新的 [a, b] 列表,也不会读取 ab 的值一样。

匹配过程

在此匹配过程中,模式的结构可能不适合主体,匹配会*失败*。

例如,将模式 Point2d(x, 0) 匹配到主体 Point2d(3, 0) 会成功匹配。匹配还将模式的自由变量 x**绑定**到主体的值 3

另一个例子是,如果主体是 [3, 0],则匹配会失败,因为主体的类型 list 不是模式的 Point2d

作为第三个例子,如果主体是 Point2d(3, 7),则匹配会失败,因为主体的第二个坐标 7 与模式的 0 不相同。

match 语句尝试将单个主体与 case 子句中的每个模式进行匹配。在第一个成功匹配 case 子句中的模式时

  • 模式中的变量将被赋值,并且
  • 将执行相应的块。

每个 case 子句还可以指定一个可选的布尔条件,称为**守卫**。

让我们看一个更详细的 match 语句示例。match 语句在函数中使用,用于定义 3D 点的构建。在此示例中,该函数可以接受以下任何一种作为输入:包含 2 个元素的元组、包含 3 个元素的元组、现有的 Point2d 对象或现有的 Point3d 对象

def make_point_3d(pt):
    match pt:
        case (x, y):
            return Point3d(x, y, 0)
        case (x, y, z):
            return Point3d(x, y, z)
        case Point2d(x, y):
            return Point3d(x, y, 0)
        case Point3d(_, _, _):
            return pt
        case _:
            raise TypeError("not a point we support")

没有模式匹配,此函数的实现将需要几个 isinstance() 检查,一个或两个 len() 调用,以及更复杂的控制流。match 示例版本和没有 match 的传统 Python 版本在底层会转换为类似的​​代码。熟悉模式匹配后,阅读使用 match 的此函数的用户可能会发现此版本比传统方法更清晰。

基本原理和目标

Python 程序经常需要处理类型、属性/键的存在或元素数量各不相同的数据。典型示例包括处理抽象语法树 (AST) 等混合结构节点、处理不同类型的 UI 事件、处理结构化输入(如结构化文件或网络消息),或“解析”可以接受不同组合类型和参数数量的函数的参数。事实上,经典的“访问者”模式就是一个 OOP 风格的示例,但匹配使其编写起来不那么乏味。

完成此操作的代码往往由复杂的嵌套 if/elif 语句组成,包括对 len()isinstance() 和索引/键/属性访问的多次调用。在这些分支中,用户有时需要进一步解构数据以提取所需的组件值,这些值可能嵌套在多个对象深处。

许多其他语言中存在的模式匹配为这个问题提供了一个优雅的解决方案。这些语言范围涵盖从静态编译的函数式语言(如 F# 和 Haskell)到混合范式语言(如 ScalaRust),再到动态语言(如 Elixir 和 Ruby),并且正在考虑用于 JavaScript。我们感谢这些语言为 Pythonic 模式匹配指明了方向,正如 Python 在许多功能上依赖于许多其他语言一样:许多基本语法特性继承自 C,异常来自 Modula-3,类受到 C++ 的启发,切片来自 Icon,正则表达式来自 Perl,装饰器类似于 Java 注释,等等。

操作异构数据的典型逻辑可以总结如下

  • 对数据*形状*(类型和组件)进行某些分析:这可能涉及 isinstance()len() 调用,以及/或通过索引或属性访问提取组件,然后检查这些组件是否具有特定值或满足特定条件。
  • 如果形状符合预期,则可能进一步提取某些组件,并使用提取的值执行某些操作。

Django Web 框架的这一部分为例

if (
    isinstance(value, (list, tuple)) and
    len(value) > 1 and
    isinstance(value[-1], (Promise, str))
):
    *value, label = value
    value = tuple(value)
else:
    label = key.replace('_', ' ').title()

我们可以看到顶部的 value 的形状分析,然后是内部的解构。

请注意,这里的形状分析涉及检查容器及其组件的类型,以及检查其元素数量。一旦匹配了形状,我们就需要分解序列。根据本 PEP 中的提案,我们可以将该代码重写为

match value:
    case [*v, label := (Promise() | str())] if v:
        value = tuple(v)
    case _:
        label = key.replace('_', ' ').title()

这种语法更明确地指出了输入数据的可能格式,以及从何处提取组件。您可以看到一种类似于列表解包的模式,但也包含类型检查:Promise() 模式不是对象构造,而是代表任何是 Promise 实例的内容。模式运算符 | 分隔替代模式(类似于正则表达式或 EBNF 文法),而 _ 是一个通配符。(请注意,此处使用的匹配语法将接受用户定义的序列,以及列表和元组。)

在某些情况下,信息提取不如识别结构重要。以 Python 标准库中的以下示例为例

def is_tuple(node):
    if isinstance(node, Node) and node.children == [LParen(), RParen()]:
        return True
    return (isinstance(node, Node)
            and len(node.children) == 3
            and isinstance(node.children[0], Leaf)
            and isinstance(node.children[1], Node)
            and isinstance(node.children[2], Leaf)
            and node.children[0].value == "("
            and node.children[2].value == ")")

此示例显示了一个查找数据“形状”而不进行大量提取的示例。此代码不易阅读,并且要匹配的预期形状并不明显。与使用提议语法的更新后的代码进行比较

def is_tuple(node: Node) -> bool:
    match node:
        case Node(children=[LParen(), RParen()]):
            return True
        case Node(children=[Leaf(value="("), Node(), Leaf(value=")")]):
            return True
        case _:
            return False

请注意,提议的代码将在不修改 Node 和其他类定义的情况下正常工作。如以上示例所示,该提案不仅支持解包序列,还支持 isinstance 检查(如 LParen()str()),查看对象属性(例如 Leaf(value="("))以及与字面量的比较。

最后一个特性有助于处理一些更像其他语言中“switch”语句的代码

match response.status:
    case 200:
        do_something(response.data)  # OK
    case 301 | 302:
        retry(response.location)  # Redirect
    case 401:
        retry(auth=get_credentials())  # Login first
    case 426:
        sleep(DELAY)  # Server is swamped, try after a bit
        retry()
    case _:
        raise RequestError("we couldn't get the data")

尽管这可以正常工作,但并非该提案的重点,并且新语法旨在最好地支持解构场景。

有关更详细的规范,请参见下文的语法部分。

我们建议可以通过新的特殊属性 __match_args__ 来自定义解构对象。作为本 PEP 的一部分,我们为一些标准库类(包括命名元组和数据类)指定了通用 API 及其实现。有关详细信息,请参见下文的运行时部分。

最后,我们旨在为静态类型检查器和类似工具提供全面的支持。为此,我们提议向 typing 模块引入一个 @typing.sealed 类装饰器,该装饰器在运行时无操作,但会向静态工具指示该类的所有子类都必须在与基类相同的模块中定义。这将允许有效的静态穷尽性检查,并与数据类一起,为代数数据类型提供基本支持。有关详细信息,请参阅静态检查器部分。

语法和语义

模式

**模式**是一种新的语法构造,可以被认为是赋值目标的松散泛化。模式的关键属性在于它接受哪些类型和形状的主体,它捕获哪些变量以及如何从主体中提取它们。例如,模式 [a, b] 只匹配恰好有 2 个元素的序列,将第一个元素提取到 a 中,第二个元素提取到 b 中。

本 PEP 定义了几种模式。这些当然不是唯一的可能性,因此决定选择一个现在有用但保守的功能子集。随着该功能得到更广泛的应用,以后可以添加更多模式。有关详细信息,请参阅被拒绝的想法推迟的想法部分。

此处列出的模式将在下文中进一步详细描述,但在此部分为了简洁而进行汇总

  • 字面量模式用于过滤结构中的常量值。它看起来像 Python 字面量(包括某些值,如 TrueFalseNone)。它仅匹配等于字面量的对象,并且从不绑定。
  • 捕获模式看起来像 x,等同于一个相同的赋值目标:它总是匹配并绑定具有给定(简单)名称的变量。
  • **通配符模式**是单个下划线: _。它总是匹配,但不捕获任何变量(这可以防止与其他用途的 _ 冲突,并允许进行一些优化)。
  • 常量值模式的功能类似于字面量,但用于某些命名常量。请注意,由于捕获模式可能存在歧义,因此它必须是限定的(带点的)名称。它看起来像 Color.RED,并且仅匹配等于相应值的值。它从不绑定。
  • 序列模式看起来像 [a, *rest, b],类似于列表解包。一个重要的区别是,嵌套其中的元素可以是任何类型的模式,而不仅仅是名称或序列。它仅匹配适当长度的序列,只要所有子模式也匹配。它执行其子模式的所有绑定。
  • 映射模式看起来像 {"user": u, "emails": [*es]}。它匹配至少包含提供的键集的映射,并且所有子模式与其对应值匹配。它在匹配键对应的​​值时,会绑定子模式绑定的所有内容。允许在模式末尾添加 **rest 来捕获额外的项。
  • 类模式类似于上述内容,但匹配属性而不是键。它看起来像 datetime.date(year=y, day=d)。它匹配给定类型的实例,具有至少指定的属性,只要属性与相应的子模式匹配。它在匹配给定属性的值时,会绑定子模式绑定的所有内容。可选协议还允许匹配位置参数。
  • OR 模式看起来像 [*x] | {"elems": [*x]}。如果其任何子模式匹配,则它将匹配。它使用匹配的左侧模式的绑定。
  • Walrus 模式看起来像 d := datetime(year=2020, month=m)。仅当其子模式也匹配时,它才会匹配。它绑定子模式匹配的内容,并将命名变量绑定到整个对象。

match 语句

提议的语法的简化近似语法是

...
compound_statement:
    | if_stmt
    ...
    | match_stmt
match_stmt: "match" expression ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" pattern [guard] ':' block
guard: 'if' expression
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: closed_pattern ('|' closed_pattern)*
closed_pattern:
    | literal_pattern
    | capture_pattern
    | wildcard_pattern
    | constant_pattern
    | sequence_pattern
    | mapping_pattern
    | class_pattern

有关完整、未经删节的语法,请参阅附录 A。本节中的简化语法是为了帮助读者,而不是作为完整的规范。

我们提议 match 操作应该是一个语句,而不是一个表达式。尽管在许多语言中它是一个表达式,但作为语句更适合 Python 语法的整体逻辑。有关更多讨论,请参阅被拒绝的想法。允许的模式将在下面的模式子部分中详细描述。

提议的 matchcase 关键字是软关键字,因此它们将在 match 语句或 case 块的开头被识别为关键字,但在其他位置可以作为变量或参数名使用。

提议的缩进结构如下

match some_expression:
    case pattern_1:
        ...
    case pattern_2:
        ...

这里,some_expression 代表正在进行匹配的值, hereafter 称为 match 的*主体*。

匹配语义

提议的大规模匹配选择语义是选择第一个匹配的模式并执行相应的块。不会尝试剩余的模式。如果没有匹配的模式,语句将“落下”,执行将继续到下一条语句。

本质上,这等同于一系列 if ... elif ... else 语句。请注意,与之前提议的 switch 语句不同,这里不适用预计算的调度字典语义。

没有 defaultelse 情况 - 相反,可以使用特殊通配符 _(参见捕获模式部分)作为最终的“捕获所有”模式。

在成功的模式匹配过程中创建的名称绑定在执行的块之后仍然存在,并且可以在 match 语句之后使用。这遵循其他可以绑定名称的 Python 语句的逻辑,例如 for 循环和 with 语句。例如

match shape:
    case Point(x, y):
        ...
    case Rectangle(x, y, _, _):
        ...
print(x, y)  # This works

在失败的模式匹配过程中,某些子模式可能会成功。例如,在将值 [0, 1, 2] 与模式 (0, x, 1) 匹配时,如果从左到右匹配列表元素,则子模式 x 可能会成功。实现可以选择使这些部分匹配的绑定持久化,或者不。包含 match 语句的用户代码不应依赖于为失败的匹配创建的绑定,但也不应假定变量在失败匹配后保持不变。此行为部分故意未指定,以便不同的实现可以添加优化,并防止引入可能限制此功能可扩展性的语义限制。

请注意,下面的某些模式类型定义了关于何时进行绑定的更具体的规则。

允许的模式

我们逐步引入提议的语法。这里我们从主要构建块开始。支持以下模式

字面量模式

简化语法

literal_pattern:
    | number
    | string
    | 'None'
    | 'True'
    | 'False'

字面量模式由简单的字面量组成,例如字符串、数字、布尔字面量(TrueFalse)或 None

match number:
    case 0:
        print("Nothing")
    case 1:
        print("Just one")
    case 2:
        print("A couple")
    case -1:
        print("One less than nothing")
    case 1-1j:
        print("Good luck with that...")

字面量模式使用右侧的字面量进行相等性比较,因此在上面的示例中将评估 number == 0,然后可能评估 number == 1 等。请注意,虽然负数在技术上使用了一元减号表示,但它们在模式匹配的目的是被视为字面量。不允许使用一元加号。二元加号和减号仅允许用于连接实数和虚数以形成复数,例如 1+1j

请注意,由于使用了相等性(__eq__),并且布尔值与整数 01 之间存在等价性,因此以下两者之间没有实际区别

case True:
    ...

case 1:
    ...

支持三引号字符串。支持原始字符串和字节字符串。不允许使用 f-string(因为它们通常不是真正的字面量)。

捕获模式

简化语法

capture_pattern: NAME

捕获模式充当匹配表达式的赋值目标

match greeting:
    case "":
        print("Hello!")
    case name:
        print(f"Hi {name}!")

只允许一个名称(带点的名称是常量值模式)。捕获模式总是成功。出现在范围内的捕获模式使该名称成为该范围内的局部变量。例如,如果在上述代码片段之后使用了 name,则如果选择了 "" case 子句,可能会引发 UnboundLocalError 而不是 NameError

match greeting:
    case "":
        print("Hello!")
    case name:
        print(f"Hi {name}!")
if name == "Santa":      # <-- might raise UnboundLocalError
    ...                  # but works fine if greeting was not empty

在匹配每个 case 子句时,一个名称最多只能绑定一次,具有两个名称重合的捕获模式是错误的

match data:
    case [x, x]:  # Error!
        ...

注意:仍然可以使用 守卫来匹配具有相同项的集合。此外,[x, y] | Point(x, y) 是合法的模式,因为两个替代项永远不会同时匹配。

单个下划线(_)不被视为 NAME,而是作为 通配符模式被特殊处理。

提醒:NoneFalseTrue 是表示字面量的关键字,而不是名称。

通配符模式

简化语法

wildcard_pattern: "_"

单个下划线(_)名称是一种特殊类型的模式,它总是匹配但*从不*绑定

match data:
    case [_, _]:
        print("Some pair")
        print(_)  # Error!

鉴于不进行绑定,可以根据需要多次使用它,这与捕获模式不同。

常量值模式

简化语法

constant_pattern: NAME ('.' NAME)+

这用于匹配常量和枚举值。模式中的每个带点名称都使用正常的 Python 名称解析规则进行查找,并使用该值与匹配主体进行相等性比较(与字面量相同)

from enum import Enum

class Sides(str, Enum):
    SPAM = "Spam"
    EGGS = "eggs"
    ...

match entree[-1]:
    case Sides.SPAM:  # Compares entree[-1] == Sides.SPAM.
        response = "Have you got anything without Spam?"
    case side:  # Assigns side = entree[-1].
        response = f"Well, could I have their Spam instead of the {side} then?"

请注意,无法使用非限定名称作为常量值模式(它们始终表示要捕获的变量)。有关曾考虑过的常量值模式的其他语法替代方案,请参阅被拒绝的想法

序列模式

简化语法

sequence_pattern:
    | '[' [values_pattern] ']'
    | '(' [value_pattern ',' [values pattern]] ')'
values_pattern: ','.value_pattern+ ','?
value_pattern: '*' capture_pattern | pattern

序列模式遵循与解包赋值相同的语义。与解包赋值一样,元组和列表语法都可以使用,并且语义相同。每个元素都可以是任意模式;最多只能有一个 *name 模式来捕获所有剩余项

match collection:
    case 1, [x, *others]:
        print("Got 1 and a nested sequence")
    case (1, x):
        print(f"Got 1 and {x}")

要匹配序列模式,主体必须是 collections.abc.Sequence 的实例,并且不能是任何类型的字符串(strbytesbytearray)。它不能是迭代器。有关匹配特定集合类的信息,请参见下面的类模式。

_ 通配符可以被星号化以匹配可变长度的序列。例如

  • [*_] 匹配任意长度的序列。
  • (_, _, *_) 匹配任何长度为 2 或更长的序列。
  • ["a", *_, "z"] 匹配任何长度为 2 或更长的序列,该序列以 "a" 开头并以 "z" 结尾。

映射模式

简化语法

mapping_pattern: '{' [items_pattern] '}'
items_pattern: ','.key_value_pattern+ ','?
key_value_pattern:
    | (literal_pattern | constant_pattern) ':' or_pattern
    | '**' capture_pattern

映射模式是可迭代解包到映射的泛化。其语法类似于字典显示,但每个键和值都是模式 "{" (pattern ":" pattern)+ "}"。也允许使用 **rest 模式来提取剩余项。只有字面量和常量值模式允许在键位置

import constants

match config:
    case {"route": route}:
        process_route(route)
    case {constants.DEFAULT_PORT: sub_config, **rest}:
        process_config(sub_config, rest)

主体必须是 collections.abc.Mapping 的实例。主体中的额外键将被忽略,即使没有存在 **rest。这与序列模式不同,序列模式中的额外项会导致匹配失败。但映射实际上与序列不同:它们具有自然的结构化子类型行为,即传递一个带有额外键的字典通常会奏效。

因此,**_ 在映射模式中是无效的;它将始终是无操作,可以毫无后果地删除。

匹配的键值对必须已存在于映射中,而不是由 __missing____getitem__ 临时创建。例如,collections.defaultdict 实例仅匹配包含在 match 块进入时已存在的键的模式。

类模式

简化语法

class_pattern:
    | name_or_attr '(' ')'
    | name_or_attr '(' ','.pattern+ ','? ')'
    | name_or_attr '(' ','.keyword_pattern+ ','? ')'
    | name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
keyword_pattern: NAME '=' or_pattern

类模式支持解构任意对象。有两种方法可以匹配对象属性:按位置,如 Point(1, 2),以及按名称,如 Point(x=1, y=2)。这两种方法可以组合,但按名称匹配不能跟在按位置匹配之后。类模式中的每个项都可以是任意模式。一个简单的例子

match shape:
    case Point(x, y):
        ...
    case Rectangle(x0, y0, x1, y1, painted=True):
        ...

匹配是否成功由 isinstance 调用的等价项决定。如果主体(示例中为 shape)不是命名类(PointRectangle)的实例,则匹配失败。否则,它将继续(有关详细信息,请参见运行时部分)。

命名类必须继承自 type。它可以是单个名称或带点的名称(例如 some_mod.SomeClassmod.pkg.Class)。前导名称不能是 _,因此例如 _(...)_.C(...) 是无效的。使用 object(foo=_) 来检查匹配的对象是否具有属性 foo

默认情况下,子模式只能通过用户定义的类的关键字进行匹配。为了支持位置子模式,需要自定义 __match_args__ 属性。运行时允许通过适当链接所有实例检查和属性查找来匹配任意嵌套的模式。

组合多个模式 (OR 模式)

多个替代模式可以使用 | 组合成一个。这意味着如果至少有一个替代模式匹配,则整个模式匹配。替代项按从左到右的顺序尝试,并且具有短路属性,如果一个匹配成功,则不会尝试后续模式。示例

match something:
    case 0 | 1 | 2:
        print("Small number")
    case [] | [_]:
        print("A short sequence")
    case str() | bytes():
        print("Something string-like")
    case _:
        print("Something else")

替代项可以绑定变量,只要每个替代项绑定相同集合的变量(不包括 _)。例如

match something:
    case 1 | x:  # Error!
        ...
    case x | 1:  # Error!
        ...
    case one := [1] | two := [2]:  # Error!
        ...
    case Foo(arg=x) | Bar(arg=x):  # Valid, both arms bind 'x'
        ...
    case [x] | x:  # Valid, both arms bind 'x'
        ...

守卫

每个*顶级*模式后面可以跟一个形式为 if expression 的**守卫**。如果模式匹配且守卫求值为真值,则 case 子句成功。例如

match input:
    case [x, y] if x > MAX_INT and y > MAX_INT:
        print("Got a pair of large numbers")
    case x if x > MAX_INT:
        print("Got a large number")
    case [x, y] if x == y:
        print("Got equal items")
    case _:
        print("Not an outstanding input")

如果求值守卫引发异常,则该异常会传播到外面,而不是使 case 子句失败。出现在模式中的名称将在守卫成功之前绑定。所以这将起作用

values = [0]

match values:
    case [x] if x:
        ...  # This is not executed
    case _:
        ...
print(x)  # This will print "0"

请注意,不允许对嵌套模式使用守卫,因此 [x if x > 0] 是一个 SyntaxError,而 1 | 2 if 3 | 4 将被解析为 (1 | 2) if (3 | 4)

Walrus 模式

通常,匹配子模式*并*将相应值绑定到名称非常有用。例如,它可以用于编写更有效的匹配,或者只是为了避免重复。为了简化这些情况,任何模式(Walrus 模式本身除外)都可以前面加上一个名称和 Walrus 运算符(:=)。例如

match get_shape():
    case Line(start := Point(x, y), end) if start == end:
        print(f"Zero length line at {x}, {y}")

Walrus 运算符左侧的名称可以在守卫、匹配块或 match 语句之后使用。但是,该名称*仅*在子模式成功时才会被绑定。另一个例子

match group_shapes():
    case [], [point := Point(x, y), *other]:
        print(f"Got {point} in the second group")
        process_coordinates(x, y)
        ...

从技术上讲,大多数此类示例都可以使用守卫和/或嵌套 match 语句重写,但这会降低可读性和/或产生效率较低的代码。基本上,PEP 572 中关于赋值表达式的参数大部分也适用于此处。

通配符 _ 在此处不是有效名称。

运行时规范

匹配协议

用于决定对象是否匹配给定类模式并提取相应属性的是 isinstance 调用的等价项。需要不同匹配语义的类(例如鸭子类型)可以通过定义 __instancecheck__(一个预先存在的元类钩子)或使用 typing.Protocol 来实现。

过程如下

  • Class(<sub-patterns>) 中,查找 Class 的类对象,并调用 isinstance(obj, Class),其中 obj 是正在匹配的值。如果为 false,则匹配失败。
  • 否则,如果以位置或关键字参数形式提供了任何子模式,则按从左到右的顺序进行匹配,如下所示。一旦子模式失败,匹配就会失败;如果所有子模式都成功,则整体类模式匹配成功。
  • 如果存在位置匹配项,并且该类具有 __match_args__ 属性,则位置 i 处的项将与通过属性 __match_args__[i] 查找的值进行匹配。例如,模式 Point2d(5, 8),其中 Point2d.__match_args__ == ["x", "y"],(大致)转换为 obj.x == 5 and obj.y == 8
  • 如果位置项的数量多于 __match_args__ 的长度,则会引发 TypeError
  • 如果匹配的类上不存在 __match_args__ 属性,并且在匹配中出现了一个或多个位置项,则也会引发 TypeError。我们不回退使用 __slots____annotations__ - “面对歧义,拒绝猜测的诱惑。”
  • 如果存在任何按关键字匹配项,则在主体的属性上查找关键字。如果查找成功,则将该值与相应的子模式进行匹配。如果查找失败,则匹配失败。

这种协议有利于实现简单性而不是灵活性和性能。有关其他考虑的替代方案,请参见扩展匹配

对于最常匹配的内置类型(boolbytearraybytesdictfloatfrozensetintlistsetstrtuple),允许将单个位置子模式传递给调用。与其匹配主体的特定属性,不如将其与主体本身进行匹配。这为这些对象创造了有用且直观的行为

  • bool(False) 匹配 False(但不匹配 0)。
  • tuple((0, 1, 2)) 匹配 (0, 1, 2)(但不匹配 [0, 1, 2])。
  • int(i) 匹配任何 int 并将其绑定到名称 i

重叠子模式

某些重叠匹配的类别会在运行时检测到,并引发异常。除了上一节中描述的基本检查之外

  • 解释器将检查两个匹配项是否未指向同一属性,例如 Point2d(1, 2, y=3) 是错误的。
  • 它还将检查映射模式是否未尝试多次匹配同一键。

特殊属性 __match_args__

__match_args__ 属性始终在模式中命名的类型对象上查找。如果存在,它必须是字符串列表或元组,命名允许的位置参数。

在决定哪些名称可用于匹配时,建议的做法是类模式应与构造相对应;也就是说,可用名称及其类型应类似于 __init__() 的参数。

默认情况下,只有按名称匹配才有效,并且类应定义 __match_args__ 作为类属性,如果它们希望支持按位置匹配。此外,数据类和命名元组将开箱即用地支持按位置匹配。有关更多详细信息,请参见下文。

异常和副作用

在匹配每个 case 时,match 语句可能会触发其他函数的执行(例如 __getitem__()len() 或属性)。由这些函数引起的几乎所有异常都会像平常一样传播到 match 语句之外。唯一不会传播异常的情况是在匹配 Class Pattern 的属性时查找属性时引发的 AttributeError;在这种情况下,它只会导致匹配失败,并且语句的其余部分正常进行。

匹配过程显式携带的唯一副作用是名称的绑定。但是,该过程依赖于属性访问、实例检查、len()、主体及其某些组件的相等性和项访问。它还评估常量值模式和类模式的左侧。虽然其中一些通常不会产生任何副作用,但其中一些对象可能会。此提案故意不指定调用了哪些方法或调用了多少次。依赖于此行为的用户代码应被视为有错误。

标准库

为了方便使用模式匹配,将对标准库进行几项更改

  • 命名元组和数据类将具有自动生成的 __match_args__
  • 对于数据类,生成的 __match_args__ 中的属性顺序将与生成的 __init__() 方法中相应参数的顺序相同。这包括从超类继承属性的情况。

此外,还将系统地审查现有的标准库类,并添加 __match_args__,只要它看起来有益。

静态检查器规范

穷尽性检查

从可靠性角度来看,经验表明,在处理一组可能的数据值时遗漏 case 会导致难以调试的问题,因此迫使人们添加类似如下的安全断言

def get_first(data: Union[int, list[int]]) -> int:
    if isinstance(data, list) and data:
        return data[0]
    elif isinstance(data, int):
        return data
    else:
        assert False, "should never get here"

PEP 484 规定静态类型检查器应支持对枚举值进行条件检查时的穷尽性。 PEP 586 后来将此要求推广到字面量类型。

本 PEP 进一步将此要求推广到任意模式。这种情况通常适用于匹配具有联合类型的表达式

def classify(val: Union[int, Tuple[int, int], List[int]]) -> str:
    match val:
        case [x, y] if x > 0 and y > 0:
            return f"A pair of {x} and {y}"
        case [x, *other]:
            return f"A sequence starting with {x}"
        case int():
            return f"Some integer"
        # Type-checking error: some cases unhandled.

穷尽性检查也应适用于模式匹配和枚举值结合的情况

from enum import Enum
from typing import Union

class Level(Enum):
    BASIC = 1
    ADVANCED = 2
    PRO = 3

class User:
    name: str
    level: Level

class Admin:
    name: str

account: Union[User, Admin]

match account:
    case Admin(name=name) | User(name=name, level=Level.PRO):
        ...
    case User(level=Level.ADVANCED):
        ...
    # Type-checking error: basic user unhandled

显然,不需要(就PEP 544而言)Matchable 协议,因为每个类都是可匹配的,因此受上述检查约束。

密封类作为代数数据类型

很多时候,我们希望对一组类应用穷尽性,而无需定义临时的联合类型,这本身就很容易出错,如果一个类在联合定义中缺失。在支持模式匹配的其他语言中,将一组记录式类组合成联合体是一种流行的设计模式,它被称为代数数据类型

我们提议向 typing 模块添加一个特殊的装饰器类 @sealed,该装饰器在运行时将不起作用,但会向静态类型检查器表明该类的所有子类(直接和间接)都必须在与基类相同的模块中定义。

其理念是,由于所有子类都是已知的,类型检查器可以将密封的基类视为其所有子类的联合。结合数据类,这可以在 Python 中实现代数数据类型的干净且安全的 T. 考虑以下示例

from dataclasses import dataclass
from typing import sealed

@sealed
class Node:
    ...

class Expression(Node):
    ...

class Statement(Node):
    ...

@dataclass
class Name(Expression):
    name: str

@dataclass
class Operation(Expression):
    left: Expression
    op: str
    right: Expression

@dataclass
class Assignment(Statement):
    target: str
    value: Expression

@dataclass
class Print(Statement):
    value: Expression

通过此类定义,类型检查器可以将 Node 安全地视为 Union[Name, Operation, Assignment, Print],并且还可以安全地将例如 Expression 视为 Union[Name, Operation]。因此,在以下代码片段中,这将导致类型检查错误,因为未处理 Name(并且类型检查器可以提供有用的错误消息)

def dump(node: Node) -> str:
    match node:
        case Assignment(target, value):
            return f"{target} = {dump(value)}"
        case Print(value):
            return f"print({dump(value)})"
        case Operation(left, op, right):
            return f"({dump(left)} {op} {dump(right)})"

类型擦除

类模式受运行时类型擦除的影响。即,尽管可以定义类型别名 IntQueue = Queue[int],使得类似 IntQueue() 的模式在语法上有效,但类型检查器应拒绝此类匹配

queue: Union[Queue[int], Queue[str]]
match queue:
    case IntQueue():  # Type-checking error here
        ...

请注意,上述代码段在当前 typing 模块中通用类的实现中,以及在最近接受的 PEP 585 中内置的通用类中,实际上在运行时会失败,因为它们禁止 isinstance 检查。

为了澄清,通用类通常不会被禁止参与模式匹配,只是它们的类型参数不能显式指定。当子模式或字面量绑定类型变量时,仍然是可以的。例如

from typing import Generic, TypeVar, Union

T = TypeVar('T')

class Result(Generic[T]):
    first: T
    other: list[T]

result: Union[Result[int], Result[str]]

match result:
    case Result(first=int()):
        ...  # Type of result is Result[int] here
    case Result(other=["foo", "bar", *rest]):
        ...  # Type of result is Result[str] here

关于常量的说明

捕获模式始终是赋值目标这一事实可能会在用户错误地尝试将值“匹配”为常量而不是使用常量值模式时产生不良后果。因此,在运行时,此类匹配将始终成功,并且更重要的是,会覆盖常量的值。因此,静态类型检查器警告这种情况非常重要。例如

from typing import Final

MAX_INT: Final = 2 ** 64

value = 0

match value:
    case MAX_INT:  # Type-checking error here: cannot assign to final name
        print("Got big number")
    case _:
        print("Something else")

请注意,CPython 参考实现还为此情况生成了 SyntaxWarning 消息。

星号匹配的精确类型检查

类型检查器应执行星号项在模式匹配中的精确类型检查,为它们提供异构的 list[T] 类型,或者提供由 PEP 589 指定的 TypedDict 类型。例如

stuff: Tuple[int, str, str, float]

match stuff:
    case a, *b, 0.5:
        # Here a is int and b is list[str]
        ...

性能考量

理想情况下,match 语句的运行时性能应优于等效的 if 语句链。尽管编程语言的历史充斥着增加工程师生产力但以牺牲额外 CPU 周期为代价的示例,但如果 match 的好处因运行时性能的显着下降而抵消,那将是遗憾的。

虽然本 PEP 没有指定任何特定的实现策略,但有必要就原型实现及其如何最大化性能进行一些说明。

基本上,原型实现将所有 match 语句语法转换为等效的 if/else 块 - 或者更准确地说,转换为具有相同效果的 Python 字节码。换句话说,用于测试实例类型、序列长度、映射键等的逻辑都内联在 match 的位置。

这不是唯一可能的策略,也未必是最好的。例如,实例检查可以进行记忆化,尤其是在单个 match 语句中存在多个相同类类型的实例但具有不同参数的情况下。理论上,未来的实现也可以使用决策树而不是逐个测试来并行处理 case 子句或子模式。

向后兼容性

本 PEP 完全向后兼容:提议的 matchcase 关键字是(并将保持!)软关键字,因此它们在不构成意义的位置(例如 x = class + 1)作为变量、函数、类、模块或属性名称的使用不受任何阻碍。

这一点很重要,因为 matchre 模块中一个流行且广为人知的函数和方法的名称,我们无意破坏或弃用它。

硬关键字和软关键字的区别在于,硬关键字*始终*是保留字,即使在它们没有意义的位置(例如 x = class + 1),而软关键字仅在上下文中具有特殊含义。由于 PEP 617,解析器会回溯,这意味着在尝试解析代码片段的不同尝试中,它可能会以不同的方式解释软关键字。

例如,假设解析器遇到以下输入

match [x, y]:

解析器首先尝试将其解析为表达式语句。它将 match 解释为 NAME 标记,然后考虑 [x, y] 是双下标。然后它遇到冒号,不得不回溯,因为表达式语句后面不能跟冒号。然后解析器回溯到行的开头,发现 match 是在此位置允许的软关键字。然后它将 [x, y] 视为列表表达式。冒号然后就是解析器所期望的,解析成功。

对第三方工具的影响

Python 生态系统中有很多工具用于处理 Python 源代码:linter、语法高亮器、自动格式化器和 IDE。所有这些都需要更新以包含对 match 语句的认识。

总的来说,这些工具分为两类

**浅层**解析器不尝试理解 Python 的完整语法,而是扫描源代码以查找特定的已知模式。IDE,如 Visual Studio Code、Emacs 和 TextMate,往往属于此类,因为源代码在编辑过程中经常是无效的,并且严格的解析方法会失败。

对于这些类型的工具,添加对新关键字的认识相对容易,只需添加到表中,或者修改一个正则表达式。

**深度**解析器理解 Python 的完整语法。自动格式化器 Black 就是一个例子。这类工具的一个特殊要求是,它们不仅需要理解当前 Python 版本的语法,还需要理解旧版本 Python 的语法。

match 语句使用软关键字,并且它是第一个主要利用新 PEG 解析器功能的主要 Python 功能之一。这意味着不“兼容 PEG”的第三方解析器将难以处理新语法。

已注意到,其中一些第三方工具利用了常见的解析库(例如,Black 使用 lib2to3 解析器的分支)。识别广泛使用的解析库(如 parsolibCST)并升级它们以兼容 PEG 可能会有所帮助。

然而,由于这项工作不仅需要针对 match 语句进行,而且需要针对*任何*利用 PEG 解析器功能的新 Python 语法进行,因此它被认为超出了本 PEP 的范围。(尽管建议这可能是一个很棒的 Summer of Code 项目。)

参考实现

可以在 GitHub 上找到一个**功能完整的 CPython 实现**:feature-complete CPython implementation

使用 BinderJupyter,基于上述实现创建了一个交互式 Playground

示例代码

可以在 GitHub 上找到一小系列示例代码

被拒绝的想法

这个普遍的想法已经酝酿了相当长的时间,并且做了许多来回的决定。在这里,我们总结了许多曾被采用但最终被放弃的替代路径。

不要这样做,模式匹配很难学

在我们看来,提议的模式匹配并不比在可迭代解包中添加 isinstance()getattr() 更困难。此外,我们认为提议的语法通过允许表达*想要做什么*,而不是*如何去做*,显著提高了各种代码模式的可读性。我们希望我们在 PEP 上方包含的几个真实代码片段足以说明这种比较。有关更多真实代码示例及其翻译,请参阅文献 [1]

不要这样做,使用现有的方法分派机制

我们认识到 match 语句的某些用例与使用类继承的传统面向对象(OOP)设计技术所能做到的重叠。基于运行时类型测试主题来选择备用行为的能力,甚至可能对严格的 OOP 纯粹主义者来说是异端邪说。

然而,Python 一直是一种拥抱各种编程风格和范式的语言。经典的 Python 设计习惯,如“鸭子”类型,超越了传统的 OOP 模型。

我们相信,在某些重要用例中,使用 match 可以实现更清晰、更易于维护的架构。这些用例往往具有以下特征

  • 跨越传统数据封装界限的算法。如果一个算法正在处理不同类型的异构元素(例如,求值或转换抽象语法树,或对数学符号进行代数运算),强制用户为每种元素类型实现单独的方法,会导致逻辑分散在整个代码库中,而不是整洁地集中在一个地方。
  • 数据类型集相对稳定,但要在这些数据类型上执行的操作集不断扩展的程序架构。严格按照面向对象的风格执行此操作,需要不断地向基类和子类添加新方法来支持新方法,用大量高度专业化的方法定义“污染”基类,并导致代码的广泛中断和变动。相比之下,基于 match 的分派,添加新行为仅涉及编写一个新的 match 语句。
  • 面向对象也不能处理基于对象“形状”的分派,例如元组的长度,或属性的存在——任何此类分派决策都必须编码到对象的类型中。基于形状的分派在处理“鸭子类型”对象时尤其有趣。

面向对象明显占优的情况恰恰相反:可能的函数集相对稳定且定义良好,但要操作的数据类型集却不断增长。UI小部件工具包是经典的例子,其中存在一组固定的交互类型(重绘、鼠标点击、按键等),但随着开发人员发明新的、富有创意的用户交互样式,小部件类型的集合不断扩展。添加一种新的窗口小部件类型很简单,只需编写一个新的子类,而使用基于匹配的方法,您最终必须为许多广泛的匹配语句添加新的 case 子句。因此,我们不建议在这种情况下使用 match

允许更灵活的赋值目标

有一个想法是直接将可迭代解包推广到更通用的赋值目标,而不是添加一种新语句。这个概念在其他一些语言中被称为“不可拒绝匹配”。我们决定不这样做,因为对现实世界中潜在用例的检查表明,在绝大多数情况下,解构与 if 条件相关。而且其中许多都分组在一系列互斥的选择中。

使其成为表达式

在大多数其他语言中,模式匹配由表达式而不是语句表示。但将其作为表达式将与 Python 中的其他语法选择不一致。所有决策逻辑几乎完全以语句形式表达,因此我们决定不偏离这一点。

使用硬关键字

有选项可以将 match 设为硬关键字,或选择其他关键字。尽管使用硬关键字可以简化简单语法高亮器的生活,但我们出于几个原因决定不使用硬关键字。

  • 最重要的是,新的解析器不需要我们这样做。与 async 不同,async 在最初的几个版本中作为软关键字带来了困难,而在这里,我们可以将 match 设为永久软关键字。
  • match 在现有代码中被广泛使用,这将破坏几乎所有现有程序,并给许多甚至可能不会从新语法中受益的人带来修复代码的负担。
  • 很难找到一个替代关键字,既不会在现有程序中被用作标识符,又能清晰地反映语句的含义。

使用 as| 而不是 case 用于 case 子句

这里提出的模式匹配是多分支控制流(类似于 Algol 派生语言中的 switch 或 Lisp 中的 cond)和函数式语言中的对象解构的结合。虽然提议的关键字 case 突出了多分支方面,但 as 等替代关键字同样可行,突出了解构方面。例如,aswith 也有作为 Python 中关键字的优势。然而,由于 case 作为关键字只能出现在 match 语句的开头,解析器很容易区分其作为关键字或变量的用法。

其他变体将使用符号,如 |=>,或者完全不使用特殊标记。

由于 Python 是 Algol 传统中的一种面向语句的语言,并且每个复合语句都以一个标识性关键字开头,所以 case 似乎与 Python 的风格和传统最一致。

使用平坦的缩进方案

有一个想法是使用替代的缩进方案,例如,每个 case 子句都不会相对于初始的 match 部分缩进。

match expression:
case pattern_1:
    ...
case pattern_2:
    ...

其动机是,虽然扁平缩进节省了一些水平空间,但对 Python 程序员来说可能看起来很奇怪,因为在其他地方,冒号后面总是跟着一个缩进。这也会使简单的代码编辑器感到麻烦。最后,通过允许匹配语句使用“半缩进”(即两个空格而不是四个空格)来缓解水平空间问题。

在作为此 PEP 开发一部分的 match 示例程序中,观察到代码简洁性有了显著的提高,这足以弥补额外的缩进级别。

另一个被考虑的提议是使用扁平缩进,但在 match: 之后的下一行放置表达式,如下所示。

match:
    expression
case pattern_1:
    ...
case pattern_2:
    ...

这最终被否决了,因为第一个块在 Python 的语法中将是一个新颖的结构:一个块的内容只有一个表达式,而不是一系列语句。

常量值模式的替代方案

这可能是最棘手的一项。匹配预定义的常量非常常见,但 Python 的动态特性也使其与捕获模式相混淆。考虑了另外五种选择。

  • 使用一些隐式规则。例如,如果一个名称是在全局范围内定义的,那么它就指代一个常量,而不是代表一个捕获模式。
    # Here, the name "spam" must be defined in the global scope (and
    # not shadowed locally). "side" must be local.
    
    match entree[-1]:
        case spam: ...  # Compares entree[-1] == spam.
        case side: ...  # Assigns side = entree[-1].
    

    然而,如果有人在 match 语句之前定义了一个不相关的同名变量,这可能会导致意外和远程影响。

  • 使用基于名称大小写的规则。特别是,如果名称以小写字母开头,它将是一个捕获模式,而如果以大写字母开头,它将指代一个常量。
    match entree[-1]:
        case SPAM: ...  # Compares entree[-1] == SPAM.
        case side: ...  # Assigns side = entree[-1].
    

    这与 PEP 8 中关于命名常量的建议非常吻合。主要反对意见是,Python 的其他核心部分没有名称的大小写在语义上是重要的。此外,Python 允许标识符使用不同的脚本,其中许多(例如 CJK)没有大小写区分。

  • 使用额外的括号来指示给定名称的查找语义。例如。
    match entree[-1]:
        case (spam): ...  # Compares entree[-1] == spam.
        case side: ...    # Assigns side = entree[-1].
    

    这可能是一个可行的选项,但如果经常使用,会产生一些视觉上的干扰。而且老实说,这看起来相当不寻常,尤其是在嵌套的上下文中。

    这也存在一个问题,即我们可能希望或需要括号来消除模式中分组的歧义,例如在 Point(x, y=(y := complex())) 中。

  • 引入一个特殊符号,例如 .?$^ 来表示一个给定的名称是要匹配的值,而不是要赋给的值。本提议的早期版本使用了前导点规则。
    match entree[-1]:
        case .spam: ...  # Compares entree[-1] == spam.
        case side: ...   # Assigns side = entree[-1].
    

    虽然可能有用,但它引入了看起来奇怪的新语法,而没有使模式语法更具表现力。事实上,通过将命名常量转换为 Enum 类型,或者将它们包含在自己的命名空间中(作者认为这是一个非常好的主意),可以使命名常量与现有规则一起工作。

    match entree[-1]:
        case Sides.SPAM: ...  # Compares entree[-1] == Sides.SPAM.
        case side: ...        # Assigns side = entree[-1].
    

    如果需要,前导点规则(或类似的变体)可以稍后添加,而不会有向后兼容性问题。

  • 还有一个想法是默认采用查找语义,并要求使用 $? 来用于捕获模式。
    match entree[-1]:
        case spam: ...   # Compares entree[-1] == spam.
        case side?: ...  # Assigns side = entree[-1].
    

    这有一些问题。

    • 在典型的代码中,捕获模式更常见,因此要求它们使用特殊语法是不受欢迎的。
    • 作者不知道是否有其他语言以这种方式装饰捕获。
    • 提议的语法没有任何一种在 Python 中有先例;Python 中其他绑定名称的地方(例如 importdeffor)都不使用特殊标记语法。
    • 这将破坏当前语法的语法平行性。
      match coords:
          case ($x, $y):
              return Point(x, y)  # Why not "Point($x, $y)"?
      

最终,这些替代方案因提到的缺点而被拒绝。

不允许在模式中使用浮点文字

由于浮点数的精度问题,本提议的早期版本不允许使用浮点常量作为匹配模式。禁止这一点的部分理由是 Rust 是这样做的。

然而,在实现过程中,发现区分浮点值和其他类型需要在虚拟机中添加额外的代码,这将普遍减慢匹配速度。鉴于 Python 和 Rust 是非常不同的语言,用户群和底层理念也不同,因此认为允许浮点文字不会造成太大危害,并且对用户来说也不会太令人惊讶。

范围匹配模式

这将允许诸如 1...6 这样的模式。然而,存在一系列歧义。

  • 范围是开的、半开的还是闭的?(即,上面的例子中是否包含 6?)
  • 范围匹配单个数字,还是范围对象?
  • 范围匹配通常用于字符范围('a'...'z'),但这在 Python 中不起作用,因为没有字符数据类型,只有字符串。
  • 范围匹配可能是重要的性能优化,如果您可以预先构建跳转表,但在 Python 中通常不可能,因为名称可以动态重新绑定。

与其创建范围的特殊语法,不如决定允许自定义模式对象(InRange(0, 6))将更灵活且不那么模糊;然而,这些想法目前已被推迟(见 推迟的想法)。

对匹配使用分派字典语义

经典 switch 语句的实现有时会使用预计算的哈希表而不是链式等式比较来提高性能。在 match 语句的上下文中,对于匹配字面量模式,技术上也可能做到这一点。然而,不同类型的模式具有细微不同的语义,对于潜在的适度性能提升来说,这会过于令人惊讶。

如果我们能够优化它们而不会造成语义差异,我们仍然可以朝着这个方向尝试可能的性能优化。

在 case 子句中使用 continuebreak

另一个被否决的提议是为 match 中的 continuebreak 定义新的含义,其行为如下:

  • continue 将退出当前的 case 子句,并继续匹配下一个 case 子句。
  • break 将退出 match 语句。

然而,这个提议有一个严重的缺点:如果 match 语句嵌套在循环中,continuebreak 的含义就改变了。这可能会导致重构过程中的意外行为;而且,有人认为有其他方法可以获得相同的行为(例如使用守卫条件),并且在实践中,continuebreak 的现有行为可能更有用。

AND (&) 模式

此提议定义了一个 OR 模式(|)来匹配多个备选项中的一个;为什么不支持 AND 模式(&)呢?特别是考虑到一些其他语言(例如 F#)支持它。

然而,它的用处尚不清楚。匹配字典、对象和序列的语义已经包含了隐含的“和”:所有提到的属性和元素都必须存在才能匹配成功。守卫条件也可以支持假设的“and”运算符将用于的许多用例。

最终,决定这将使语法更复杂,而没有增加显著的好处。

负匹配模式

使用前缀 ! 对匹配模式的否定将正好匹配,如果模式本身不匹配。例如,!(3 | 4) 将匹配除了 34 之外的任何内容。

这被拒绝了,因为有 书面证据 表明此功能很少有用(在支持它的语言中)或用作双重否定 !! 来控制变量作用域并防止变量绑定(这不适用于 Python)。它也可以通过守卫条件来模拟。

在运行时检查穷尽性

问题是,如果没有任何 case 子句匹配模式,并且没有默认 case,应该怎么做?本提议的早期版本规定,在这种情况下,行为将是抛出异常而不是静默失败。

双方争论了很多,但最终 EIBTI(显式优于隐式)论点占了上风:如果程序员想要抛出异常,最好让他们明确地抛出异常。

对于密封类和枚举等情况,其中所有模式都已知是离散集合的成员,静态检查器 可以警告缺失的模式。

模式变量的类型注解

提议将模式与类型注解结合起来。

match x:
    case [a: int, b: str]: print(f"An int {a} and a string {b}:)
    case [a: int, b: int, c: int]: print(f"Three ints", a, b, c)
    ...

这个想法有很多问题。首先,冒号只能在方括号或圆括号内使用,否则语法会变得含糊不清。而且由于 Python 不允许对泛型类型进行 isinstance() 检查,因此包含泛型的类型注解将无法按预期工作。

允许在类模式中使用 *rest

提议允许在类模式中使用 *rest,将一个变量绑定到所有位置参数,一次性绑定(类似于其在解包赋值中的用法)。它将与序列模式提供一定的对称性。但它可能会与提供所有位置参数的 *值* 的功能混淆。而且似乎没有实际需要,所以它被搁置了。(如果出现需求,以后很容易添加。)

不允许在常量值模式中使用 _.a

第一个公开草案指出,常量值模式中的初始名称不能是 _,因为 _ 在模式匹配中有特殊含义,因此这将是无效的。

case _.a: ...

(然而,a._ 将是合法的,并将像往常一样加载对象 a 的名称为 _ 的属性。)

这在 python-dev 上受到了一些反对(有些人确实有正当理由将 _ 作为重要的全局变量,尤其是在 i18n 中),而禁止这一点的唯一原因是为了防止一些用户混淆。但这并不是需要坚持的原则。

使用其他标记作为通配符

有人提议使用 ...(即省略号标记)或 *(星号)作为通配符。然而,这两者看起来都像省略了任意数量的项。

case [a, ..., z]: ...
case [a, *, z]: ...

两者看起来都像匹配两个或更多项的序列,捕获第一个和最后一个值。

此外,如果 * 被用作通配符字符,我们将不得不找到其他方法来捕获序列的其余部分,目前是这样写的。

case [first, second, *rest]: ...

使用省略号在文档和示例中也会更令人困惑,因为 ... 通常用于表示显而易见或无关紧要的内容。(是的,这也将是反对 ... 在 Python 中的其他用途的论据,但那已经是过去式了。)

另一项提议是使用 ?。这可能是可以接受的,尽管它需要修改词法分析器。

此外,_ 已经在其他上下文中用作弃用目标,这种用法非常相似。这个例子来自标准库中的 difflib.py

for tag, _, _, j1, j2 in group: ...

也许最有说服力的论点是,在其他支持模式匹配的语言中,_ 都用作通配符:C#、Elixir、Erlang、F#、Haskell、Mathematica、OCaml、Ruby、Rust、Scala 和 Swift。现在,总的来说,我们不应该过于关注其他语言的做法,因为 Python 显然与所有这些语言都不同。然而,如果存在如此压倒性的共识,Python 不应该特意去做一些完全不同的事情——特别是考虑到 _ 在 Python 中运行良好并且已经被用作弃用目标。

请注意,_ 不会被模式赋值——这避免了与 _ 用作可翻译字符串标记和 gettext.gettext 的别名(正如 gettext 模块文档所推荐的那样)发生冲突。

使用其他语法而不是 | 来表示 OR 模式

有人提出了几种替代方案来使用 | 来分隔 OR 模式中的备选项。而不是

case 401|403|404:
    print("Some HTTP error")

以下提议已被提出。

  • 使用逗号。
    case 401, 403, 404:
      print("Some HTTP error")
    

    这看起来太像元组了——我们必须找到一种不同的方式来拼写元组,并且该结构必须在类模式的参数列表中用括号括起来。总的来说,Python 中的逗号已经有了很多不同的含义,我们不应该再添加更多。

  • 允许堆叠 case。
    case 401:
    case 403:
    case 404:
      print("Some HTTP error")
    

    在 C 中,这是通过其 case 的 fall-through 语义来实现的。然而,我们不想误导人们认为 match/case 使用 fall-through 语义(这是 C 中常见的错误来源)。此外,这将是一种新颖的缩进模式,可能使其在 IDE 等中更难支持(它会破坏“在以冒号结尾的行后添加缩进级别”的简单规则)。最后,这不支持嵌套在其他模式中的 OR 模式。

  • 使用 case in 后跟逗号分隔的列表。
    case in 401, 403, 404:
      print("Some HTTP error")
    

    这对于嵌套在其他模式中的 OR 模式将不起作用,例如:

    case Point(0|1, 0|1):
        print("A corner of the unit square")
    
  • 使用 or 关键字。
    case 401 or 403 or 404:
        print("Some HTTP error")
    

    这可以做到,而且可读性与使用 | 没有太大区别。一些用户表示偏爱 or,因为他们将 | 与按位或相关联。然而。

    1. 许多其他具有模式匹配功能的语言使用 |(列表包括 Elixir、Erlang、F#、Mathematica、OCaml、Ruby、Rust 和 Scala)。
    2. | 更短,这可能有助于提高嵌套模式的可读性,例如 Point(0|1, 0|1)
    3. 有些人错误地认为 | 的优先级不正确;但由于模式不支持其他运算符,它的优先级与表达式中的优先级相同。
    4. Python 用户非常频繁地使用 or,并且可能认为它与布尔短路紧密相关。
    5. | 用于正则表达式和 EBNF 语法(如 Python 本身)中的备选项。
    6. | 不仅用于按位或——它用于集合并集、字典合并(PEP 584)并被考虑作为 typing.Union 的替代项(PEP 604)。
    7. | 作为视觉分隔符效果更好,尤其是在字符串之间。比较。
      case "spam" or "eggs" or "cheese":
      

      case "spam" | "eggs" | "cheese":
      

添加 else 子句

我们出于几个原因决定不添加 else 子句。

  • 它是多余的,因为我们已经有了 case _:
  • 关于 else: 的缩进级别将永远存在混淆——它应该与 case 列表对齐还是与 match 关键字对齐?
  • “每个其他语句都有一个”这样的完成主义论点是错误的——只有那些 else 子句的语句在其添加了新功能。

延迟的想法

有一些提议来扩展匹配语法,我们决定推迟到未来的 PEP。这些属于“很棒但非必需的想法”的范畴,并且认为在就其中一些提议迈出步伐之前,最好先获得一些关于 match 语句实际使用情况的实际数据。

请注意,在每种情况下,这个想法都被判定为“双向门”,这意味着稍后添加这些功能不会有向后兼容性问题。

一次性语法变体

在检查了一些可能最受益于提议语法的代码库时,发现单子句匹配相对常用,主要用于各种特殊情况。在其他语言中,这以一次性匹配的形式得到支持。我们也提议支持这种一次性匹配。

if match value as pattern [and guard]:
    ...

或者,选择不带 if

match value as pattern [if guard]:
    ...

相当于以下展开。

match value:
    case pattern [if guard]:
        ...

为了说明这如何提高可读性,请考虑以下(略微简化的)真实代码片段。

if isinstance(node, CallExpr):
    if (isinstance(node.callee, NameExpr) and len(node.args) == 1 and
            isinstance(node.args[0], NameExpr)):
        call = node.callee.name
        arg = node.args[0].name
        ...  # Continue special-casing 'call' and 'arg'
...  # Follow with common code

这可以重写为更直接的方式。

if match node as CallExpr(callee=NameExpr(name=call), args=[NameExpr(name=arg)]):
    ...  # Continue special-casing 'call' and 'arg'
...  # Follow with common code

这种一次性形式不允许 elif match 语句,因为它只用于处理单个模式情况。它旨在成为 match 语句的特例,而不是 if 语句的特例。

if match value_1 as patter_1 [and guard_1]:
    ...
elif match value_2 as pattern_2 [and guard_2]:  # Not allowed
    ...
elif match value_3 as pattern_3 [and guard_3]:  # Not allowed
    ...
else:  # Also not allowed
    ...

这将破坏一次性匹配作为详尽完全匹配的补充的目的——在这种情况下,使用完整匹配更好、更清晰。

类似地,不允许 if not match,因为 match ... as ... 不是表达式。我们也不提议像某些支持模式匹配的语言那样存在 while match 构造,因为尽管它可能很方便,但它可能很少使用。

其他基于模式的构造

许多其他支持模式匹配的语言将其作为多种语言构造的基础,包括匹配运算符、赋值的通用形式、循环的过滤器、同步通信的方法或专门的 if 语句。其中一些已在对第一稿的讨论中提到。另一个被问到的问题是,为什么选择这种特定的形式(连接绑定和条件选择),而其他形式没有被选择。

鉴于我们在使用模式方面的经验,引入更多模式的使用将过于大胆和不成熟,并会使此提议过于复杂。所呈现的语句提供了一种足够通用且有用的功能形式,同时保持自成一体,并且对整个语言的语法和语义没有产生巨大影响。

在获得该功能的一些经验后,社区可能会对其他有价值的模式匹配用途有更好的认识。

重复名称的代数匹配

在 Erlang 和 Elixir 等函数式语言中偶尔会看到一种技术,即在同一模式中使用匹配变量多次。

match value:
    case Point(x, x):
        print("Point is on a diagonal!")

这里的想法是,x 的第一次出现会将值绑定到名称,后续的出现将验证传入的值是否等于先前绑定的值。如果值不相等,则匹配将失败。

然而,在混合捕获模式的加载-存储语义方面存在一些细微之处。目前,我们决定将同一个模式中的名称重复使用视为错误;我们可以随时放宽此限制,而不会影响向后兼容性。

请注意,您*可以*在备选选择中使用相同的名称一次以上。

match value:
    case x | [x]:
        # etc.

自定义匹配协议

在本次 PEP 的初步设计讨论中,有很多关于自定义匹配器的想法。这有几个动机:

  • 一些类可能希望公开与实际类属性不同的“可匹配”名称集。
  • 一些类可能具有昂贵的计算属性,因此除非匹配模式实际需要访问它们,否则不应进行评估。
  • 有关于奇特的匹配器的想法,如 IsInstance()InRange()RegexMatchingGroup() 等等。
  • 为了让内置类型和标准库类能够以合理且直观的方式支持匹配,人们认为这些类型需要实现特殊的匹配逻辑。

这些定制的匹配行为将由类名上的特殊 __match__ 方法控制。有两个竞争的变体。

  • 一个“全功能”匹配协议,它不仅会传递要匹配的主题,还会传递指定模式感兴趣的属性的详细信息。
  • 一个简化的匹配协议,它只传递主题值,并返回一个“代理对象”(在大多数情况下可以是主题本身),其中包含可匹配的属性。

这是一个更复杂的提议协议的例子。

match expr:
    case BinaryOp(left=Number(value=x), op=op, right=Number(value=y)):
        ...

from types import PatternObject
BinaryOp.__match__(
    (),
    {
        "left": PatternObject(Number, (), {"value": ...}, -1, False),
        "op": ...,
        "right": PatternObject(Number, (), {"value": ...}, -1, False),
    },
    -1,
    False,
)

该协议的一个缺点是 __match__ 的参数构建成本高昂,并且无法预先计算,因为由于名称绑定的方式,Python 中没有真正的常量。这也意味着 __match__ 方法将不得不重新实现匹配逻辑的大部分,而否则这些逻辑将在 Python 虚拟机中使用 C 代码实现。因此,与等效的 if 语句相比,此选项的性能会较差。

更简单的协议的缺点是,尽管它性能更好,但灵活性要差得多,并且不允许人们梦想到的许多富有创意的自定义匹配器。

然而,在设计过程的后期,人们意识到对自定义匹配协议的需求远低于预期。几乎所有现实的(而不是异想天开的)提出的用例都可以通过内置的匹配行为来处理,尽管在少数情况下需要额外的守卫条件才能达到期望的效果。

此外,事实证明,除了适当的 __match_args__ 属性之外,没有一个标准库类真正需要任何特殊的匹配支持。

推迟此功能的决定伴随着这样一个认识:这不是一个单向门;更灵活和可定制的匹配协议可以稍后添加,特别是当我们获得更多关于实际用例和实际用户需求的实际经验时。

本 PEP 的作者期望 match 语句将随着使用模式和习语的演变而随时间演变,类似于过去其他“多阶段” PEP 的情况。届时,可以重新审视扩展匹配问题。

参数化匹配语法

(也称为“类实例匹配器”。)

这是“自定义匹配类”概念的另一种变体,它允许前面部分提到的各种自定义匹配器——然而,不是使用扩展的匹配协议,而是通过引入具有自己语法的附加模式类型来实现。这种模式类型将接受两组不同的参数:一组由传递给模式对象构造函数的实际参数组成,另一组表示模式的绑定变量。

这些对象的 __match__ 方法可以使用构造函数参数值来决定什么是有效匹配。

这将允许 InRange<0, 6>(value) 这样的模式,它将匹配 0..6 范围内的数字并将匹配的值赋给“value”。类似地,可以有一个模式来测试正则表达式匹配结果中命名组的存在(“match”一词的不同含义)。

尽管有对这个想法的一些支持,但在语法上进行了大量的细节争论(没有很多有吸引力的选择),并且没有达成明确的共识,所以决定目前这个功能对 PEP 来说不是必不可少的。

模式实用库

前两个想法都将伴随一个新的 Python 标准库模块,其中将包含一套丰富的有用匹配器。然而,实际上不可能在不采纳前面部分给出的一个扩展模式提议的情况下实现这样的库,所以这个想法也被推迟了。

致谢

我们感谢以下个人(以及许多其他人)在撰写此 PEP 的各个阶段提供的帮助。

  • Gregory P. Smith
  • Jim Jewett
  • Mark Shannon
  • Nate Lust
  • Taine Zhao

版本历史

  1. 初始版本
  2. 大幅重写,包括:
    • 小幅澄清、语法和拼写错误修正。
    • 重命名各种概念。
    • 对被否决想法的额外讨论,包括:
      • 为什么我们选择 _ 作为通配符模式。
      • 为什么我们选择 | 作为 OR 模式。
      • 为什么我们选择不使用捕获变量的特殊语法。
      • 为什么选择这种模式匹配操作而不是其他操作。
    • 澄清异常和副作用语义。
    • 澄清部分绑定语义。
    • 删除在加载上下文中使用 _ 的限制。
    • 删除默认单个位置参数是整个主题(除了少数内置类型)的限制。
    • 简化 __match_args__ 的行为。
    • 删除 __match__ 协议(移至 推迟的想法)。
    • 删除 ImpossibleMatchError 异常。
    • 删除加载的前导点(移至 推迟的想法)。
    • 重写了初始部分(语法之前的所有内容)。
    • 在详细描述之前添加了所有模式类型的概述。
    • 在每个模式描述旁边添加了简化的语法。
    • 将通配符与捕获模式分开描述。
    • 添加 Daniel F Moisset 作为第六位共同作者。

参考资料

附录 A – 完整语法

这里是 match_stmt 的完整语法。这是 compound_stmt 的一个附加替代项。应该理解,matchcase 是软关键字,也就是说,它们在其他语法上下文中不是保留字(包括在行首,如果预期没有冒号)。按照惯例,硬关键字使用单引号,软关键字使用双引号。

标准 EBNF 之外使用的其他符号。

  • SEP.RULE+RULE (SEP RULE)* 的简写
  • !RULE 是否定先行断言
match_expr:
    | star_named_expression ',' star_named_expressions?
    | named_expression
match_stmt: "match" match_expr ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" patterns [guard] ':' block
guard: 'if' named_expression
patterns: value_pattern ',' [values_pattern] | pattern
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: '|'.closed_pattern+
closed_pattern:
    | capture_pattern
    | literal_pattern
    | constant_pattern
    | group_pattern
    | sequence_pattern
    | mapping_pattern
    | class_pattern
capture_pattern: NAME !('.' | '(' | '=')
literal_pattern:
    | signed_number !('+' | '-')
    | signed_number '+' NUMBER
    | signed_number '-' NUMBER
    | strings
    | 'None'
    | 'True'
    | 'False'
constant_pattern: attr !('.' | '(' | '=')
group_pattern: '(' patterns ')'
sequence_pattern: '[' [values_pattern] ']' | '(' ')'
mapping_pattern: '{' items_pattern? '}'
class_pattern:
    | name_or_attr '(' ')'
    | name_or_attr '(' ','.pattern+ ','? ')'
    | name_or_attr '(' ','.keyword_pattern+ ','? ')'
    | name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
signed_number: NUMBER | '-' NUMBER
attr: name_or_attr '.' NAME
name_or_attr: attr | NAME
values_pattern: ','.value_pattern+ ','?
items_pattern: ','.key_value_pattern+ ','?
keyword_pattern: NAME '=' or_pattern
value_pattern: '*' capture_pattern | pattern
key_value_pattern:
    | (literal_pattern | constant_pattern) ':' or_pattern
    | '**' capture_pattern

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

最后修改:2025-02-01 08:55:40 GMT