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

Python 增强提案

PEP 642 – 结构化模式匹配的显式模式语法

作者:
Alyssa Coghlan <ncoghlan at gmail.com>
BDFL 委托

讨论至:
Python-Dev 列表
状态:
已拒绝
类型:
标准跟踪
要求:
634
创建日期:
2020年9月26日
Python 版本:
3.10
发布历史:
2020年10月31日,2020年11月8日,2021年1月3日
决议:
Python-Dev 消息

目录

摘要

本 PEP 涵盖了 PEP 634 的结构化模式匹配的替代语法提案,该提案要求所有捕获模式和值约束都有显式前缀。它还提出了一种新的专用语法用于实例属性模式,该语法更贴合拟议的映射模式语法。

虽然结果必然比 PEP 634 中的拟议语法更冗长,但它仍然比现状大大不冗长。

例如,以下 match 语句将从 2 个项的序列、一个具有“host”和“port”键的映射、任何具有“host”和“port”属性的对象,或者一个“host:port”字符串中提取“host”和“port”详细信息,并将最后一个情况中的“port”视为可选。

port = DEFAULT_PORT
match expr:
    case [as host, as port]:
        pass
    case {"host" as host, "port" as port}:
        pass
    case {"host" as host}:
        pass
    case object{.host as host, .port as port}:
        pass
    case object{.host as host}:
        pass
    case str{} as addr:
        host, __, optional_port = addr.partition(":")
        if optional_port:
            port = optional_port
    case __ as m:
        raise TypeError(f"Unknown address format: {m!r:.200}")
port = int(port)

总的来说,本 PEP 建议将各种可用模式类型分类如下:

  • 通配符模式: __
  • 分组模式: (PTRN)
  • 值约束模式
    • 相等约束: == EXPR
    • 同一性约束: is EXPR
  • 结构约束模式
    • 序列约束模式: [PTRN, as NAME, PTRN as NAME]
    • 映射约束模式: {EXPR: PTRN, EXPR as NAME}
    • 实例属性约束模式: CLS{.NAME, .NAME: PTRN, .NAME == EXPR, .NAME as NAME}
    • 类定义的约束模式: CLS(PTRN, PTRN, **{.NAME, .NAME: PTRN, .NAME == EXPR, .NAME as NAME})
  • OR 模式: PTRN | PTRN | PTRN
  • AS 模式: PTRN as NAME (省略模式表示 __

此方法的目的是

  • 允许在不预先确定处理裸名称、属性查找和字面量值的最佳默认选项的情况下,开发和发布一种模式匹配的初始形式
  • 确保模式匹配在抽象语法树级别显式定义,从而允许将模式匹配的语义和表面语法清晰地分开
  • 定义清晰简洁的“鸭子类型”语法,该语法可能在普通表达式中采用,以便更轻松地从同一对象检索包含多个属性的元组

相对于 PEP 634,该提案还故意消除了任何不使用 as 关键字而“绑定到右侧”的语法(在 PEP 634 的映射模式和类模式中使用捕获模式),或在同一模式中同时绑定到左侧和右侧(使用 PEP 634 的捕获模式与 AS 模式)。

与其他 PEP 的关系

本 PEP 既依赖于 PEP 634,又与其竞争——PEP 作者同意 match 语句将是语言中一个非常有价值的补充,值得它为学习过程增加的额外复杂性,但不同意“简单名称与字面量或属性查找”的概念真的能为 match 模式中的名称绑定和值查找操作提供足够的语法区分(至少对于 Python 而言)。

本 PEP 同意 PEP 640 的精神(即跳过名称绑定的选定通配符模式应在所有地方支持,而不仅仅是在 match 模式中),但现在为通配符语法提出了不同的拼写(__ 而不是 ?)。因此,它与 PEP 640 按原样编写相竞争,但会补充一个提案,该提案将弃用 __ 作为普通标识符的使用,而是将其转换为通用的通配符标记,该标记始终跳过创建新的局部变量绑定。

虽然尚未正式提出 PEP,但 Mark Shannon 有一个预 PEP 草案 [8],其中表达了对 PEP 634 中模式匹配提案运行时语义的几点担忧。本 PEP 在某种程度上是对该提案的补充,因为尽管本 PEP 主要关注表面语法更改而不是主要语义更改,但它确实建议使抽象语法树定义更明确,以便更好地将表面语法细节与代码生成步骤的语义分开。该预 PEP 草案中有一个特定的想法是本 PEP 明确拒绝的:不同类型的匹配是相互排斥的想法。同一个值完全可能匹配不同类型的结构模式,哪种优先将有意地由 match 语句中的 case 顺序决定。

动机

最初的 PEP 622(后来拆分为 PEP 634PEP 635PEP 636)在其语法设计中包含了一个未说明但至关重要的假设:普通表达式现有的赋值目标语法都不能为 match 模式中使用的语法提供足够的基础。

虽然 PEP 没有明确说明这个假设,但一位 PEP 作者在 python-dev [1] 上清晰地解释了它:

我看到的实际问题是,我们有不同的文化/直觉在根本上发生冲突。特别是,许多程序员欢迎模式匹配作为“扩展的 switch 语句”,因此发现名称绑定而不是用于比较的表达式很奇怪。其他人则认为它与当前的赋值语句不符,并质疑为什么点状名称是绑定的。所有群体似乎共同的一点是,他们将他们的对新 match 语句的理解和解释称为“一致”或“直观”——自然地指出我们作为 PEP 作者在设计上出了什么问题。

但关键在于:至少在 Python 世界中,本 PEP 提议的模式匹配是一种前所未有、新颖的处理常见问题的方式。它不仅仅是现有内容的扩展。更糟的是:在设计 PEP 时,我们发现无论从哪个角度来看,都会遇到看似“不一致”的问题(也就是说,模式匹配不能有意义地归结为现有 Python 功能的“线性”扩展):总有一些东西从根本上超越了 Python 中已有的东西。这就是为什么我认为基于“直观”或“一致”的论点在这种情况下没有意义。

因此,本 PEP 的第一个迭代诞生于试图证明第二个论点不准确,并且 match 模式可以作为赋值目标的变体来处理,而不会导致固有的矛盾。(之前提交给原始 PEP 622 的“拒绝的想法”部分的一个早期 PR 曾被拒绝 [2])。

然而,本 PEP 的审查过程强烈表明, Tobias 在他的电子邮件中提到的矛盾不仅存在,而且足够令人担忧,以至于对 PEP 634 中提出的语法提案产生了怀疑。因此,本 PEP 进行了更改,其程度甚至超过了 PEP 634,并在很大程度上放弃了序列匹配语法与现有可迭代解包语法的对齐(有效地回答了 DLS’20 论文 [9] 中提出的第一个问题:“我们能否扩展类似可迭代解包的功能来处理更通用的对象和数据布局?”——“不完全,至少在精确语法方面”)。

这导致了 PEP 目标的完全逆转: PEP 不再试图强调赋值和模式匹配之间的相似性,而是现在试图确保赋值目标语法根本不被重用,从而降低了基于现有构造的经验对新构造产生错误推断的可能性。

最后,在完成提案的第三次迭代(完全删除了推断模式)之前,PEP 作者花了相当多的时间反思 PEP 20 中的以下条目:

  • 显式优于隐式。
  • 特殊情况不足以打破规则。
  • 面对歧义,拒绝猜测的诱惑。

如果我们从显式语法开始,以后总可以添加语法快捷方式(例如,考虑到最近添加 UnionOptional 类型提示快捷方式的提案,是在使用了多年原始更冗长形式之后才提出的),而如果我们只从缩写形式开始,那么我们就没有任何真正的方法在将来的版本中重新审视这些决定。

规范

本 PEP 保留了 PEP 634 中整体的 match/case 语句结构和语义,但提出了多项更改,意味着用户意图在具体语法中显式指定,而不是需要从模式匹配上下文中推断。

在拟议的抽象语法树中,语义也始终是显式的,无需推断。

match 语句

表面语法

match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
subject_expr:
    | star_named_expression ',' star_named_expressions?
    | named_expression
case_block: "case" (guarded_pattern | open_pattern) ':' block

guarded_pattern: closed_pattern 'if' named_expression

open_pattern:
    | as_pattern
    | or_pattern

closed_pattern:
    | wildcard_pattern
    | group_pattern
    | structural_constraint

抽象语法

Match(expr subject, match_case* cases)
match_case = (pattern pattern, expr? guard, stmt* body)

star_named_expression, star_named_expressions, named_expressionblock 规则是 标准 Python 语法 的一部分。

开放模式是包含多个标记的模式,不一定以结束定界符终止(例如,__ as x, int() | bool())。为避免人类读者产生歧义,其使用仅限于顶层模式和分组模式(即被括号包围的模式)。

封闭模式是那些要么由单个标记组成(即 __),要么将结束定界符作为其语法必需部分(例如 [as x, as y], object{.x as x, .y as y})。

PEP 634 一样,matchcase 关键字是软关键字,即它们在其他语法上下文中(包括在一行开头但没有预期的冒号时)不是保留字。这意味着它们仅在作为 match 语句或 case 块的一部分时才被识别为关键字,而在所有其他上下文中允许用作变量或参数名称。

PEP 634 不同,模式被显式定义为抽象语法树中的新节点类型——即使表面语法与现有表达式节点共享,解析器也会发出一个不同的抽象节点。

作为参考,match_stmt 是表面语法中 compound_statement 的新替代项,而 Match 是抽象语法中 stmt 的新替代项。

match 语义

本 PEP 大致保留了 PEP 634 中提出的整体模式匹配语义。

模式的拟议语法发生重大变化,下文将详细讨论。

此外,还对类定义的约束(PEP 634 中的类模式)的语义进行了一些提议的更改,以消除对任何内置类型的特殊处理的需要(相反,引入用于实例属性约束的专用语法允许将这些内置类型所需的行为指定为适用于设置了 __match_args__None 的任何类型)。

守卫

本 PEP 保留了 PEP 634 中提出的 guard 子句语义。

但是,语法略有更改,要求当存在 guard 子句时,case 模式必须是封闭模式。

这使读者更清楚模式的结束位置和 guard 子句的开始位置。(这主要是 OR 模式的一个潜在问题,其中 guard 子句看起来有点像最后一个模式中的条件表达式的开始。实际上这样做不是合法语法,所以对编译器而言没有歧义,但这种区别对人类读者来说可能不那么清晰。)

不可否认的 case 块

PEP 634 相比,不可否认 case 块的定义在本 PEP 中略有更改,因为捕获模式不再是 AS 模式的一个独立概念。

除了这个注意点之外,不可否认情况的处理与 PEP 634 中相同。

  • 通配符模式是不可否认的
  • 其左侧是不可否认的 AS 模式
  • 包含至少一个不可否认模式的 OR 模式
  • 带括号的不可否认模式
  • 当 case 块没有 guard 且其模式是不可否认的时,该 case 块被认为是不可否认的。
  • 一个 match 语句最多可以有一个不可否认的 case 块,并且它必须是最后一个。

模式

模式的顶层表面语法如下:

open_pattern: # Pattern may use multiple tokens with no closing delimiter
    | as_pattern
    | or_pattern

as_pattern: [closed_pattern] pattern_as_clause

or_pattern: '|'.simple_pattern+

simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
    | closed_pattern
    | value_constraint

closed_pattern: # Require a single token or a closing delimiter in pattern
    | wildcard_pattern
    | group_pattern
    | structural_constraint

如上所述,开放模式的使用仅限于顶层 case 子句和分组模式中的括号内。

模式的抽象语法显式地指出了哪些元素是子模式,哪些元素是子表达式或标识符。

pattern = MatchAlways
     | MatchValue(matchop op, expr value)
     | MatchSequence(pattern* patterns)
     | MatchMapping(expr* keys, pattern* patterns)
     | MatchAttrs(expr cls, identifier* attrs, pattern* patterns)
     | MatchClass(expr cls, pattern* patterns, identifier* extra_attrs, pattern* extra_patterns)

     | MatchRestOfSequence(identifier? target)
     -- A NULL entry in the MatchMapping key list handles capturing extra mapping keys

     | MatchAs(pattern? pattern, identifier target)
     | MatchOr(pattern* patterns)

AS 模式

表面语法

as_pattern: [closed_pattern] pattern_as_clause
pattern_as_clause: 'as' pattern_capture_target
pattern_capture_target: !"__" NAME !('.' | '(' | '=')

(注意:右侧的名称不能是 __。)

抽象语法

MatchAs(pattern? pattern, identifier target)

AS 模式将 as 关键字左侧的封闭模式与 subject 进行匹配。如果失败,AS 模式失败。否则,AS 模式将 subject 绑定到 as 关键字右侧的名称并成功。

如果未给出要匹配的模式,则隐含通配符模式(__)。

为避免与通配符模式混淆,双下划线(__)不允许作为捕获目标(这正是 !"__" 所表达的)。

捕获模式总是成功的。它使用 PEP 572 中为命名表达式建立的名称绑定范围规则,将 subject 值绑定到名称。(摘要:该名称成为最近包含函数范围内的局部变量,除非有适用的 nonlocalglobal 语句。)

在给定的模式中,给定的名称最多只能绑定一次。这不允许例如 case [as x, as x]: ...,但允许 case [as x] | (as x)

作为开放模式,AS 模式的使用仅限于顶层 case 子句和分组模式内的括号中。但是,几个结构约束允许在相关位置使用 pattern_as_clause 将匹配 subject 的提取值绑定到局部变量。这些主要在抽象语法树中表示为 MatchAs 节点,除了序列模式中专用的 MatchRestOfSequence 节点。

OR 模式

表面语法

or_pattern: '|'.simple_pattern+

simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
    | closed_pattern
    | value_constraint

抽象语法

MatchOr(pattern* patterns)

当两个或多个模式由竖线(|)分隔时,这称为 OR 模式。(单个简单模式就是那个模式)

只有最后一个子模式可以是不可否认的。

每个子模式必须绑定相同的名称集。

OR 模式按顺序将每个子模式与 subject 进行匹配,直到有一个成功。然后,OR 模式被视为成功。如果所有子模式都失败,则 OR 模式失败。

子模式大多被要求为封闭模式,但值约束可以省略括号。

值约束

表面语法

value_constraint:
    | eq_constraint
    | id_constraint

eq_constraint: '==' closed_expr
id_constraint: 'is' closed_expr

closed_expr: # Require a single token or a closing delimiter in expression
    | primary
    | closed_factor

closed_factor: # "factor" is the main grammar node for these unary ops
    | '+' primary
    | '-' primary
    | '~' primary

抽象语法

MatchValue(matchop op, expr value)
matchop = EqCheck | IdCheck

primary 规则定义在标准 Python 语法中,并且只允许由单个标记组成,或者需要以结束定界符结尾的表达式。

值约束取代了 PEP 634 的字面量模式和值模式。

相等约束写为 == EXPR,而同一性约束写为 is EXPR

当 subject 值与右侧给定的值相等时,相等约束成功,而同一性约束仅在它们是完全相同的对象时才成功。

要与之比较的表达式主要限于单个标记(例如,名称、字符串、数字、内置常量),或者限制为需要以结束定界符结尾的表达式。

高优先级的一元运算符也是允许的,因为感知歧义的风险较低,并且能够指定负数而无需括号是可取的。

当同一个约束表达式在同一个 match 语句中出现多次时,解释器可能会缓存第一个计算值并重用它,而不是重复表达式求值。(对于 PEP 634 的值模式,此缓存严格绑定到给定 match 语句的给定执行。)

PEP 634 中的字面量模式不同,本 PEP 要求复杂的字面量必须用括号括起来才能被解析器接受。有关这一点,请参阅“推迟的想法”部分。

如果本 PEP 被采纳并优先于 PEP 634,那么所有字面量和值模式将改为更明确地写成值约束:

# Literal patterns
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...")

# Additional literal patterns
match value:
    case == True:
        print("True or 1")
    case == False:
        print("False or 0")
    case == None:
        print("None")
    case == "Hello":
        print("Text 'Hello'")
    case == b"World!":
        print("Binary 'World!'")

# Matching by identity rather than equality
SENTINEL = object()
match value:
    case is True:
        print("True, not 1")
    case is False:
        print("False, not 0")
    case is None:
        print("None, following PEP 8 comparison guidelines")
    case is ...:
        print("May be useful when writing __getitem__ methods?")
    case is SENTINEL:
        print("Matches the sentinel by identity, not just value")

# Matching against variables and attributes
from enum import Enum
class Sides(str, Enum):
    SPAM = "Spam"
    EGGS = "eggs"
    ...

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

注意 == preferred_side 示例:使用显式前缀标记来表示约束表达式消除了仅限于使用属性或字面量进行值查找的限制。

== (1-1j) 示例说明了如何使用括号将任何子表达式转换为封闭表达式。

通配符模式

表面语法

wildcard_pattern: "__"

抽象语法

MatchAlways

通配符模式总是成功的。与 PEP 634 一样,它不绑定任何名称。

PEP 634 选择单个下划线作为其通配符模式以与其他语言保持一致时,本 PEP 选择双下划线,因为它在潜在地跨整个语言保持一致方面有更清晰的路径,而对于 "_" 来说,这条路径被 i18n 相关的用例所阻碍。

使用示例

match sequence:
    case [__]:               # any sequence with a single element
        return True
    case [start, *__, end]:  # a sequence with at least two elements
        return start == end
    case __:                 # anything
        return False

分组模式

表面语法

group_pattern: '(' open_pattern ')'

有关 open_pattern 的语法,请参见上面的“模式”。

带括号的模式没有额外的语法,也不会在抽象语法树中表示。它允许用户在模式周围添加括号以强调预期的分组,并在语法需要封闭模式时嵌套开放模式。

PEP 634 不同,序列模式没有潜在的歧义,因为本 PEP 要求所有序列模式都用方括号编写。

结构约束

表面语法

structural_constraint:
    | sequence_constraint
    | mapping_constraint
    | attrs_constraint
    | class_constraint

注意:单独的“结构约束”子类别不在抽象语法树中使用,它仅用作表面语法定义中的一个方便的分组节点。

结构约束是用于对复杂对象进行断言和从中提取值的模式。

这些模式都可以绑定多个值,无论是通过嵌套的 AS 模式,还是通过模式定义中包含的 pattern_as_clause 元素。

序列约束

表面语法

sequence_constraint: '[' [sequence_constraint_elements] ']'
sequence_constraint_elements: ','.sequence_constraint_element+ ','?
sequence_constraint_element:
    | star_pattern
    | simple_pattern
    | pattern_as_clause
star_pattern: '*' (pattern_as_clause | wildcard_pattern)

simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
    | closed_pattern
    | value_constraint

pattern_as_clause: 'as' pattern_capture_target

抽象语法

MatchSequence(pattern* patterns)

MatchRestOfSequence(identifier? target)

序列约束允许检查序列中的项,并可选地提取它们。

如果 subject 值不是 collections.abc.Sequence 的实例,序列模式将失败。如果 subject 值是 strbytesbytearray 的实例,它也会失败(有关可能消除这种特殊情况的讨论,请参阅“推迟的想法”)。

一个序列模式最多可以包含一个星号子模式。星号子模式可以出现在任何位置,并在 AST 中使用 MatchRestOfSequence 节点表示。

如果没有星号子模式,则序列模式是固定长度序列模式;否则,它是可变长度序列模式。

固定长度序列模式在 subject 序列的长度不等于子模式数量时失败。

可变长度序列模式在 subject 序列的长度小于非星号子模式的数量时失败。

subject 序列的长度使用内置的 len() 函数(即通过 __len__ 协议)获得。但是,解释器可能会以与值约束表达式所述类似的方式缓存此值。

固定长度序列模式从左到右将子模式匹配到 subject 序列的相应项。一旦子模式匹配失败,匹配就会停止(并失败)。如果所有子模式都成功匹配了相应的项,则序列模式成功。

可变长度序列模式首先将前导非星号子模式匹配到 subject 序列的相应项,就像固定长度序列一样。如果成功,星号子模式将匹配一个由剩余 subject 项组成的列表,其中项从末尾移除,对应于星号子模式后面的非星号子模式。然后,将剩余的非星号子模式匹配到相应的 subject 项,就像固定长度序列一样。

子模式大多被要求为封闭模式,但值约束可以省略括号。序列元素也可以在不带括号的情况下无条件捕获。

注意:虽然 PEP 634 允许与赋值语句中的可迭代解包具有相同的语法灵活性,但本 PEP 将序列模式专门限制为方括号形式。考虑到开放式和括号式比方括号在可迭代解包中更受欢迎,这有助于强调可迭代解包和序列匹配不是相同的操作。它还避免了括号式在单元素序列模式和分组模式之间的歧义问题。

映射约束

表面语法

mapping_constraint: '{' [mapping_constraint_elements] '}'
mapping_constraint_elements: ','.key_value_constraint+ ','?
key_value_constraint:
    | closed_expr pattern_as_clause
    | closed_expr ':' simple_pattern
    | double_star_capture
double_star_capture: '**' pattern_as_clause

(注意:**__ 被此语法故意禁止,因为附加的映射条目默认被忽略)

closed_expr 定义在上面,在值约束下。

抽象语法

MatchMapping(expr* keys, pattern* patterns)

映射约束允许检查序列中的键和值,并可选地提取值。

如果 subject 值不是 collections.abc.Mapping 的实例,映射模式将失败。

如果映射模式中给出的每个键都存在于 subject 映射中,并且每个键的模式都匹配 subject 映射的相应项,则映射模式成功。

使用具有两个参数的 get 方法和一个唯一的哨兵值来检查键的存在,这提供了以下好处:

  • 查找过程无需创建异常
  • 实现了 __missing__ 的映射(如 collections.defaultdict)仅匹配它们已包含的键,它们不会隐式添加键

映射模式不得包含重复的键值。如果在检查映射模式时检测到重复键,则该模式被视为无效,并引发 ValueError。虽然理论上可以在编译时检查重复的常量键,但目前没有定义或实现此类检查。

(注意:此语义描述源自 PEP 634 的参考实现,该实现与撰写本文时 PEP 634 的规范文本不同。该实现似乎是合理的,因此修改 PEP 文本似乎是解决差异的最佳方法。)

如果存在 '**' as NAME 双星号模式,则该名称将绑定到一个 dict,该字典包含 subject 映射中的任何剩余键值对(如果没有任何其他键值对,字典将为空)。

映射模式最多可以包含一个双星号模式,并且它必须是最后一个。

值子模式大多被要求为封闭模式,但值约束可以省略括号(仍需要 : 键/值分隔符,以确保条目看起来不像普通的比较操作)。

映射值也可以使用 KEY as NAME 形式无条件捕获,无需括号或 : 键/值分隔符。

实例属性约束

表面语法

attrs_constraint:
    | name_or_attr '{' [attrs_constraint_elements] '}'
attrs_constraint_elements: ','.attr_value_pattern+ ','?
attr_value_pattern:
    | '.' NAME pattern_as_clause
    | '.' NAME value_constraint
    | '.' NAME ':' simple_pattern
    | '.' NAME

抽象语法

MatchAttrs(expr cls, identifier* attrs, pattern* patterns)

实例属性约束允许检查实例的类型并可选地提取属性。

实例属性约束不得多次重复相同的属性名。尝试这样做将导致语法错误。

如果 subject 不是 name_or_attr 的实例,实例属性模式将失败。这是使用 isinstance() 进行测试的。

如果 name_or_attr 不是内置 type 的实例,将引发 TypeError

如果没有实例属性子模式,则当 isinstance() 检查成功时,约束将成功。否则

  • 给定的每个属性名都将作为属性在 subject 上查找。
    • 如果这引发了除 AttributeError 之外的异常,则该异常会向上冒泡。
    • 如果这引发了 AttributeError,则约束失败。
    • 否则,与关键字关联的子模式将与属性值进行匹配。如果未指定子模式,则假定为通配符模式。如果失败,则约束失败。如果成功,则匹配继续到下一个属性。
  • 如果所有属性子模式都成功,则整个约束成功。

实例属性约束允许通过使用 object 作为必需的实例类型来实现鸭子类型检查(例如 case object{.host as host, .port as port}:)。

此处提议的语法可能还可用作在一次赋值语句中检索对象实例多个属性的新语法的依据(例如 host, port = addr{.host, .port})。有关此点的进一步讨论,请参阅“推迟的想法”部分。

类定义的约束

表面语法

class_constraint:
    | name_or_attr '(' ')'
    | name_or_attr '(' positional_patterns ','? ')'
    | name_or_attr '(' class_constraint_attrs ')'
    | name_or_attr '(' positional_patterns ',' class_constraint_attrs] ')'
positional_patterns: ','.positional_pattern+
positional_pattern:
    | simple_pattern
    | pattern_as_clause
class_constraint_attrs:
    | '**' '{' [attrs_constraint_elements] '}'

抽象语法

MatchClass(expr cls, pattern* patterns, identifier* extra_attrs, pattern* extra_patterns)

类定义的约束允许在类上指定一组常见的属性,并按位置进行检查,而无需在每个相关 match 模式中指定属性名。

与实例属性模式一样:

  • 如果 subject 不是 name_or_attr 的实例,类定义的模式将失败。这是使用 isinstance() 进行测试的。
  • 如果 name_or_attr 不是内置 type 的实例,将引发 TypeError

无论是否有参数,都会使用等效于 getattr(cls, "__match_args__", _SENTINEL)) 的方式检查 subject 是否具有 __match_args__ 属性。

如果这引发了异常,则该异常会向上冒泡。

如果返回的值不是列表、元组或 None,则转换失败,并在运行时引发 TypeError

这意味着只有实际定义了 __match_args__ 的类型才能在类定义的模式中使用。未定义 __match_args__ 的类型仍可在实例属性模式中使用。

如果 __match_args__None,则只允许一个位置子模式。尝试指定额外的属性模式(无论是位置的还是使用双星号语法)都会在运行时引发 TypeError

此位置子模式然后与整个 subject 进行匹配,允许将类型检查与另一个匹配模式结合起来(例如,检查容器的类型和内容,或数字的类型和值)。

如果 __match_args__ 是列表或元组,则类定义的约束将转换为实例属性约束,如下所示:

  • 如果仅存在双星号属性约束子模式,则匹配过程与等效的实例属性约束相同。
  • 如果位置子模式的数量多于 __match_args__ 的长度(使用 len() 获取),则引发 TypeError
  • 否则,位置模式 i 将使用 __match_args__[i] 作为属性名转换为属性模式。
  • 如果 __match_args__ 中的任何元素不是字符串,则引发 TypeError
  • 一旦位置模式被转换为属性模式,它们就会与双星号属性约束子模式中给出的任何属性约束结合起来,然后匹配过程与等效的实例属性约束相同。

注意:本 PEP 中的 __match_args__ is None 处理取代了 PEP 634 中对 bool, bytearray, bytes, dict, float, frozenset, int, list, set, str, 和 tuple 的特殊处理。然而,对于那些类型的优化快速路径在实现中得以保留。

设计讨论

要求在 match 模式中显式限定简单名称

本 PEP 的第一个迭代接受了 PEP 634 的基本前提,即可迭代解包语法可以为定义新的模式匹配语法提供良好的基础。

然而,在审查过程中,直接源于这个核心假设的两个主要和一处次要的歧义问题被凸显出来:

  • 最具问题的是,当默认绑定简单名称扩展到 PEP 634 提出的类模式语法时,ATTR=TARGET_NAME 构造在不使用 as 关键字的情况下绑定到右侧,并使用正常的“赋值到左侧”的符号(=)来实现这一点!
  • 当默认绑定简单名称扩展到 PEP 634 提出的映射模式语法时,KEY: TARGET_NAME 构造在不使用 as 关键字的情况下绑定到右侧。
  • PEP 634 的捕获模式与 AS 模式一起使用(TARGET_NAME_1 as TARGET_NAME_2)会产生一种奇怪的“同时绑定到左侧和右侧”的行为。

本 PEP 的第三次修订通过放弃与可迭代解包语法的对齐来解决这个问题,转而要求除了变量查找之外,所有对裸简单名称的使用都必须由前导符号或关键字限定。

  • as NAME:局部变量绑定
  • .NAME:属性查找
  • == NAME:变量查找
  • is NAME:变量查找
  • 任何其他用法:变量查找

这种方法的主要好处是它使模式中的简单名称解释成为一个局部活动:前导 as 表示名称绑定,前导 . 表示属性查找,其他任何都是变量查找(无论我们是读取子模式还是子表达式)。

有了本 PEP 中现在提出的语法,上面确定的有问题的案例就不再显得糟糕了:

  • .ATTR as TARGET_NAMEATTR=TARGET_NAME 更明显是绑定。
  • KEY as TARGET_NAMEKEY: TARGET_NAME 更明显是绑定。
  • (as TARGET_NAME_1) as TARGET_NAME_2TARGET_NAME_1 as TARGET_NAME_2 更明显是两次绑定。

抵制猜测的诱惑

PEP 635 探讨了其他语言中模式匹配的使用方式,并试图利用这些信息来对 Python 中模式匹配的使用方式做出合理的预测。

  • 希望将值提取到局部名称将希望匹配存储在局部名称中的值更常见。
  • 希望通过相等性进行比较将希望通过同一性进行比较更常见。
  • 用户可能至少能记住裸名称绑定值而属性引用查找值,即使他们自己无法自行找出这一点,而需要阅读文档或听别人告知。

需要明确的是,我认为这些预测确实是合理的。但是,我认为我们也不必在开始时就猜测:我认为我们可以从一个更显式的语法开始,要求用户使用前缀标记(as==,或 is)来陈述他们的意图,然后根据模式匹配在Python中实际的使用方式,过几年再重新评估情况。

届时,我们将能够至少在以下选项中进行选择:

  • 决定显式语法足够简洁,并且不进行任何更改
  • None...TrueFalse 添加推断的同一性约束
  • 为其他字面量(可能包括复杂字面量)添加推断的相等性约束
  • 添加推断的相等性约束或属性查找
  • 为裸名称添加推断的相等性约束或推断的捕获模式

所有这些想法都可以根据其自身的优点独立考虑,而不是作为引入模式匹配本身的潜在障碍。

如果最终引入了任何这些语法快捷方式,它们也应该很容易根据底层的更显式语法进行解释(前导的 as==,或 is 只是被解析器推断出来,而用户无需显式提供)。在实现层面,只需要更改解析器,因为现有的 AST 节点可以重用。

与局部变量中属性查找的缓存交互

本 PEP 与 PEP 634 之间的一个主要区别是使用 == EXPR 进行相等约束查找,而不是仅提供 NAME.ATTR。最初的动机是避免与常规赋值目标产生语义冲突,因为在赋值语句中 NAME.ATTR 已用于设置属性,所以如果 NAME.ATTR 是符号值匹配的唯一语法,那么我们就预先排除了未来使用现有赋值语句语法匹配单个模式的任何尝试。目前的动机更多是出于避免猜测用户意图的普遍愿望,而是要求他们在语法中显式说明。

然而,即使在 match 语句本身中,用于值模式的 name.attr 语法也与局部变量赋值存在不良交互,因为对于任何其他 Python 语句而言,语义中性的常规重构应用于 PEP 634 风格的 match 语句时,会引入重大的语义更改。

考虑以下代码:

while value < self.limit:
    ... # Some code that adjusts "value"

属性查找可以安全地从循环中移出,并且只执行一次。

_limit = self.limit:
while value < _limit:
    ... # Some code that adjusts "value"

使用本 PEP 中的标记前缀语法,值约束也将同样容忍 match 模式被重构为使用局部变量而不是属性查找,以下两个语句将具有相同的功能:

match expr:
    case {"key": == self.target}:
        ... # Handle the case where 'expr["key"] == self.target'
    case __:
        ... # Handle the non-matching case

_target = self.target
match expr:
    case {"key": == _target}:
        ... # Handle the case where 'expr["key"] == self.target'
    case __:
        ... # Handle the non-matching case

相比之下,在使用 PEP 634 的值和捕获模式语法(省略了标记前缀)时,以下两个语句将完全不等价:

# PEP 634's value pattern syntax
match expr:
    case {"key": self.target}:
        ... # Handle the case where 'expr["key"] == self.target'
    case _:
        ... # Handle the non-matching case

# PEP 634's capture pattern syntax
_target = self.target
match expr:
    case {"key": _target}:
        ... # Matches any mapping with "key", binding its value to _target
    case _:
        ... # Handle the non-matching case

本 PEP 确保在这种简单的重构风格下保留原始语义:使用 == name 强制将结果解释为值约束,使用 as name 进行名称绑定。

PEP 634 仅提供简写语法的提案,没有显式前缀形式,这意味着提供的主要答案是“好吧,别那样做,只与命名空间中的属性进行比较,不要与简单名称进行比较”。

PEP 622 的海象模式语法与它可能不绑定与 case 子句主体中相同的海象表达式相同的对象存在另一个奇怪的交互,但 PEP 634 通过将海象模式替换为 AS 模式来修复了这种差异(其中绑定到 RHS 名称的值可能与 LHS 返回的值不同,这是“as”关键字所有用法中的标准特性)。

使用现有的比较运算符作为值约束前缀

如果接受专用值约束前缀的好处,那么下一个问题是确切的前缀应该是什么。

本 PEP 最初发布的版本建议使用以前未使用的 ? 符号作为相等约束的前缀,以及 ?is 作为同一性约束的前缀。在审查 PEP 时,Steven D’Aprano 提出了一个有力的反提案 [5],建议使用现有的比较运算符(==is)代替。

== 作为前缀方面存在一些顾虑,使其在 PEP 的初始迭代中未能被选为前缀:

  • 对于常见用例,它比 ? 更显眼,因为许多受 PEP 8 风格训练的审美人士希望在它和后面的表达式之间加上空格,有效地使其成为一个 3 个字符的前缀而不是 1 个。
  • 在映射模式中使用时,需要在 : 键/值分隔符和 == 前缀之间留一个空格,否则分词器会错误地将它们分开(得到 :== 而不是 :==)。
  • 在 OR 模式中使用时,需要在 | 模式分隔符和 == 前缀之间留一个空格,否则分词器会错误地将它们分开(得到 |== 而不是 |==)。
  • 如果在 PEP 634 风格的类模式中使用,需要在 = 关键字分隔符和 == 前缀之间留一个空格,否则分词器会错误地将它们分开(得到 === 而不是 ===)。

与其引入一个全新的符号,Steven 提出的解决这个冗长问题的办法是,在语法上不含糊的情况下,保留省略前缀标记的能力。

虽然省略前缀标记的想法在提案的第二次修订中被接受,但由于歧义问题,在第三次修订中再次被删除。取而代之的是,以下几点适用:

  • 对于类模式,其他语法更改允许相等约束写为 .ATTR == EXPR,同一性约束写为 .ATTR is EXPR,这两者都非常易读。
  • 对于映射模式,额外的语法噪音只是被容忍(至少目前如此)。
  • 对于 OR 模式,额外的语法噪音只是被容忍(至少目前如此)。然而,成员资格约束可能为减少将 OR 模式与相等约束结合使用的需要提供一条未来途径(取而代之的是,要检查的值将被收集为集合、列表或元组)。

从这个角度来看,PEP 635 中反对使用 ? 作为模式匹配语法一部分的论点也适用于此提案,因此 PEP 已相应修改。

使用 __ 作为通配符模式标记

PEP 635 提出了一个有力的论点,即? 作为通配符模式标记是一个坏主意。随着值约束的语法从使用 ??is 改为使用现有的比较运算符,这个论点也适用于本 PEP。

然而,正如 Thomas Wouters 在 [6] 中指出的,PEP 634 选择 _ 仍然存在问题,因为它可能意味着 match 模式与 Python 的所有其他部分存在永久差异——_ 在软件国际化和交互式提示中的使用意味着,将其用作通用的“跳过绑定”标记确实没有可行的路径。

__ 是另一个“不需要此值”的标记,取自一篇关于 _ 在现有 Python 代码中的各种含义的 Stack Overflow 回答 [7](最初由本 PEP 作者发布)。

本 PEP 还提议采用一种实现技术,该技术将 __ 的相关特殊处理范围限制在解析器:定义一个新的 AST 节点类型(MatchAlways)专门用于通配符标记,而不是将其作为 Name 节点传递到 AST。

在解析器内部,__ 仍然意味着在 match 模式中是常规名称还是通配符标记,具体取决于你在解析树中的位置,但在编译器其余部分中,Name("__") 仍然是一个普通变量名,而 MatchAlways() 在 match 模式中始终是一个通配符标记。

_ 不同,__ 没有其他用例,这意味着通过使 __ 在 Python 的所有地方都表示“跳过此名称绑定”,可以有可行的路径来恢复标识符处理与语言其余部分的一致性。

  • 在解释器本身中,弃用具有名称 __ 的变量加载。这将使读取 __ 发出弃用警告,而写入它最初将保持不变。为了避免减慢所有名称加载的速度,这可以通过让编译器为已弃用的名称发出额外代码来处理,而不是在标准名称加载操作码中使用运行时检查。
  • 在经过适当数量的发布后,将解析器更改为为所有使用 __ 作为赋值目标的情况发出新的 SkippedBinding AST 节点,并相应地更新编译器其余部分。
  • 考虑将 __ 设为真正的硬关键字而不是软关键字。

对于 _,无法遵循此弃用路径,因为解释器无法区分尝试在名义上用作“不关心”标记时读取 _,以及将 _ 作为 i18n 文本翻译函数或交互式提示中的最后一个语句结果的合法读取。

以双下划线开头的名称也已保留用于语言使用,无论是用于编译时常量(即 __debug__)、特殊方法,还是类属性名称改编,因此在此处使用 __ 与该现有方法一致。

在抽象语法树中显式表示模式

PEP 634 没有明确讨论 match 语句应如何在抽象语法树中表示,而是将该细节留给实现来定义。

结果是,尽管 PEP 634 的参考实现确实有效(并且构成了此 PEP 参考实现的基础),但它包含一个重大的设计缺陷:尽管 PEP 635 中有关于模式应与表达式区分开的说明,但参考实现却继续将它们表示为表达式节点。

结果是 AST 并不那么抽象:本应完全以不同方式编译的节点(因为它们是模式而不是表达式)却被表示为相同的方式,并且实现语言(例如 CPython 的 C)的类型系统无法提供任何帮助来跟踪哪些子节点应该是普通表达式而哪些应该是子模式。

因此,本 PEP 没有继续采用该方法,而是定义了一个新的显式“模式”节点在 AST 中,它允许模式及其允许的子节点在 AST 本身中显式定义,从而使实现新功能的代码更清晰,并允许 C 编译器在跟踪代码生成器何时处理模式或表达式时提供更多帮助。

这种实现方法的更改实际上与本 PEP 中提出的表面语法更改是正交的,因此即使 PEP 的其余部分被拒绝,它仍然可以被采纳。

序列模式的更改

本 PEP 相对于 PEP 634 对序列模式做了一个显著的更改:

  • 仅支持方括号形式的序列模式。不支持开放式(无分隔符)或元组风格(括号作为分隔符)的序列模式。

相对于 PEP 634,序列模式也受到要求显式限定捕获模式和值约束的更改的显著影响,因为这意味着 case [a, b, c]: 必须写成 case [as a, as b, as c]:,而 case [0, 1]: 必须写成 case [== 0, == 1]:

随着序列模式的语法不再直接源于可迭代解包的语法,保留最初仅为与可迭代解包保持一致而包含在语法提案中的语法灵活性也变得没有意义。

允许开放式和元组式序列模式并没有增加表达力,只会增加意图的歧义(尤其是在与分组模式相比时),并鼓励读者沿着将模式匹配语法视为与赋值目标语法内在相关的路径前进(PEP 634 作者已多次声明这不是读者应采取的可取路径,并且这是本 PEP 作者现在同意的观点,尽管他最初不同意)。

映射模式的更改

本 PEP 相对于 PEP 634 对映射模式做了两项显著更改:

  • 值捕获写为 KEY as NAME 而不是 KEY: NAME
  • 允许更广泛的键:任何“封闭表达式”,而不是仅限于字面量和属性引用。

如上所述,第一项更改是确保所有目标名称在子表达式或模式右侧的绑定操作都使用 as 关键字。

第二项更改主要是一个简化解析器和代码生成器代码的问题,通过重用现有的表达式处理机制。对封闭表达式的限制旨在帮助减少关于键表达式在哪里结束以及匹配模式在哪里开始的歧义。这主要是允许 PEP 634 允许的超集,除了复杂的字面量必须用括号括起来(至少目前如此)。

PEP 635 的映射模式示例改编为本 PEP 提出的语法:

match json_pet:
    case {"type": == "cat", "name" as name, "pattern" as pattern}:
        return Cat(name, pattern)
    case {"type": == "dog", "name" as name, "breed" as breed}:
        return Dog(name, breed)
    case __:
        raise ValueError("Not a suitable pet")

def change_red_to_blue(json_obj):
    match json_obj:
        case { 'color': (== 'red' | == '#FF0000') }:
            json_obj['color'] = 'blue'
        case { 'children' as children }:
            for child in children:
                change_red_to_blue(child)

供参考,等效的 PEP 634 语法:

match json_pet:
    case {"type": "cat", "name": name, "pattern": pattern}:
        return Cat(name, pattern)
    case {"type": "dog", "name": name, "breed": breed}:
        return Dog(name, breed)
    case _:
        raise ValueError("Not a suitable pet")

def change_red_to_blue(json_obj):
    match json_obj:
        case { 'color': ('red' | '#FF0000') }:
            json_obj['color'] = 'blue'
        case { 'children': children }:
            for child in children:
                change_red_to_blue(child)

类模式的更改

本 PEP 相对于 PEP 634 对类模式做了几项显著更改:

  • 与类实例化的句法对齐被放弃,因为它具有误导性且无益。相反,引入了一种新的专用句法来检查附加属性,该句法从映射模式而非类实例化中汲取灵感。
  • 引入了一种新的专用句法,用于简单的鸭子类型检查,该检查适用于任何类。
  • 各种内置和标准库类型的特殊处理,通过检查是否存在值为 None__match_args__ 属性来补充。

如上所述,第一个更改有两个目的:

  • 它确保了子表达式或模式右侧所有与目标名称的绑定操作都使用 as 关键字。使用 = 来向右赋值尤其存在问题。
  • 它确保了模式中所有简单名称的使用都带有指示其用途的前缀(在这种情况下,是一个前导 . 来表示属性查找)。

与类实例化的句法对齐也被认为普遍无益,因为类模式是关于根据属性匹配模式,而类实例化是关于根据类构造函数中的参数匹配调用参数,这可能与生成的实例属性根本不相似。

第二个更改旨在使模式匹配更容易用于 Python 中已常见的“鸭子类型”风格检查。

这些模式的具体句法提案随后源于将实例视为属性名称到值的映射,并将属性查找句法(.ATTR)与映射模式句法({KEY: PATTERN})结合起来,得到 cls{.ATTR: PATTERN}

允许 cls{.ATTR} 的含义与 cls{.ATTR: __} 相同,这取决于将前导 . 视为足以使名称使用明确(它清楚地是一个属性引用,而与映射模式中的变量键进行匹配可能存在歧义)。

最后一次更改只是补充了 PEP 634 参考实现中仅 CPython 内部使用的检查,将其设为默认行为,即如果类不定义 __match_args__,则使用此行为(PEP 634 中命名的内置和标准库类型的优化快速路径得以保留)。

改编 PEP 635 中链接的类匹配示例表明,对于纯粹的位置类匹配,主要影响来自值约束和名称绑定的更改,而不是类匹配本身的更改。

match expr:
    case BinaryOp(== '+', as left, as right):
        return eval_expr(left) + eval_expr(right)
    case BinaryOp(== '-', as left, as right):
        return eval_expr(left) - eval_expr(right)
    case BinaryOp(== '*', as left, as right):
        return eval_expr(left) * eval_expr(right)
    case BinaryOp(== '/', as left, as right):
        return eval_expr(left) / eval_expr(right)
    case UnaryOp(== '+', as arg):
        return eval_expr(arg)
    case UnaryOp(== '-', as arg):
        return -eval_expr(arg)
    case VarExpr(as name):
        raise ValueError(f"Unknown value of: {name}")
    case float() | int():
        return expr
    case __:
        raise ValueError(f"Invalid expression value: {repr(expr)}")

供参考,等效的 PEP 634 语法:

match expr:
    case BinaryOp('+', left, right):
        return eval_expr(left) + eval_expr(right)
    case BinaryOp('-', left, right):
        return eval_expr(left) - eval_expr(right)
    case BinaryOp('*', left, right):
        return eval_expr(left) * eval_expr(right)
    case BinaryOp('/', left, right):
        return eval_expr(left) / eval_expr(right)
    case UnaryOp('+', arg):
        return eval_expr(arg)
    case UnaryOp('-', arg):
        return -eval_expr(arg)
    case VarExpr(name):
        raise ValueError(f"Unknown value of: {name}")
    case float() | int():
        return expr
    case _:
        raise ValueError(f"Invalid expression value: {repr(expr)}")

类模式本身的更改对于根据命名属性进行检查和提取其值而不依赖于 __match_args__ 更加相关。

match expr:
    case object{.host as host, .port as port}:
        pass
    case object{.host as host}:
        pass

将其与 PEP 634 等效项进行比较,其中不清楚哪些名称引用匹配主题的属性,哪些名称引用局部变量。

match expr:
    case object(host=host, port=port):
        pass
    case object(host=host):
        pass

在这种特定情况下,这种歧义无关紧要(因为属性名和变量名相同),但在一般情况下,知道哪个是哪个对于正确理解所阅读的代码至关重要。

延迟的想法

推断的值约束

如上所述,本 PEP 不排除将来添加推断的相等性和同一性约束的可能性。

这些可能对字面量特别有价值,因为很有可能许多具有不言自明含义的“魔术”字符串和数字将直接写入匹配模式,而不是存储在命名变量中。(例如,常量 None,或显而易见的特殊数字,如 01,或者其内容与任何变量名一样具有描述性的字符串,而不是对不透明数字的神秘检查,如 739452

使一些必需的括号成为可选

本 PEP 目前倾向于要求用括号括起来,以避免潜在的歧义。

然而,在一些情况下,它至少可以说是走得太远了,主要是涉及带有显式模式的 AS 模式。

在任何需要封闭模式的位置,AS 模式可能最终以双括号开头,因为嵌套模式也需要是封闭模式:((OPEN PTRN) as NAME)

由于子模式需要是封闭的,因此在许多情况下(例如序列模式子模式),接受 CLOSED_PTRN as NAME 应该是合理的。

对此的进一步考虑已推迟,因为允许可选的必需括号是一项向后兼容的更改,因此以后可以逐案考虑放宽限制。

接受复杂的字面量作为封闭表达式

PEP 634 的参考实现包含了在解析器和编译器其余部分中对二进制操作的许多特殊处理,以便接受复杂的字面量而不接受字面量值上的任意二进制数值运算。

理想情况下,这个问题将在解析器层处理,解析器将直接发出一个 Constant AST 节点,并预先填充一个复数。如果情况如此,那么复数字面量就可以通过与任何其他字面量类似的机制来接受。

然而,复数字面量并非如此处理。相反,它们被传递到 AST 中作为普通的 BinOp 节点,然后 AST 上的常量折叠过程将它们解析为具有复数值的 Constant 节点。

为了让解析器直接解析复数字面量,编译器需要能够指示分词器为虚数生成一个不同的标记类型(例如 INUMBER),这将允许解析器将 NUMBER + INUMBERNUMBER - INUMBER 与其他二进制操作分开处理。

或者,可以定义一个新的 ComplexNumber AST 节点类型,它允许解析器通知后续的编译器阶段,某个节点应该是一个复数字面量,而不是一个任意的二进制操作。然后,解析器可以接受 NUMBER + NUMBERNUMBER - NUMBER 作为该节点,同时让 ComplexNumber 的 AST 验证确保字面量的实部和虚部是预期的实数和虚数。

目前,本 PEP 已推迟处理此问题,而是要求复数字面量必须用括号括起来才能在值约束和映射模式键中使用。

允许在 match 模式中使用否定约束

使用本 PEP 中提出的句法,不允许将 != expris not expr 写入匹配模式。

这两种形式都有明确的潜在解释,即否定相等性约束(即 x != expr)和否定同一性约束(即 x is not expr)。

然而,这两种形式是否会足够频繁地出现以证明专用句法的合理性仍然不清楚,因此在进一步的社区对 match 语句的经验之前,已推迟了可能的扩展。

允许在 match 模式中使用成员资格检查

用于相等性和同一性约束的句法很容易扩展到成员资格检查:in container

本 PEP 和 PEP 634 中提案的一个缺点是,在同一个 case 中检查多个值看起来不像 Python 中任何现有的容器成员资格检查。

# PEP 634's literal patterns
match value:
    case 0 | 1 | 2 | 3:
        ...

# This PEP's equality constraints
match value:
    case == 0 | == 1 | == 2 | == 3:
        ...

在本 PEP 下允许推断的相等性约束只会使其看起来像 PEP 634 的示例,它仍然不像等效的 if 语句头(if value in {0, 1, 2, 3}:)。

成员资格约束将提供一种更明确但仍然简洁的方法来检查匹配主题是否存在于容器中,并且它看起来与普通的包含检查相同。

match value:
    case in {0, 1, 2, 3}:
        ...
    case in {one, two, three, four}:
        ...
    case in range(4): # It would accept any container, not just literal sets
        ...

此类功能还可以轻松扩展,允许所有类型的 case 子句,而无需进一步的句法更新,只需在自定义类定义上适当地定义 __contains__ 即可。

然而,虽然这似乎是一个有用的扩展,并且是解决本 PEP 在 OR 模式中组合多个相等性检查时的冗长问题的好方法,但它对于使 match 语句成为语言的有价值的补充不是必需的,因此将其推迟到单独的提案似乎更合适,而不是包含在这里。

为实例属性约束推断默认类型

用于实例属性约束的专用句法意味着可以从 object{.ATTR} 中省略 object,得到 {.ATTR},而不会引入任何句法歧义(如果没有给出类,则会像类定义中的基类列表一样,暗示 object)。

然而,节省六个字符是否值得让映射模式和实例属性模式在视觉上更难区分,这一点尚不清楚,因此允许这样做已被推迟为可能未来考虑的主题。

避免序列模式中的特殊情况

本 PEP 和 PEP 634 中的序列模式目前都特殊处理 strbytesbytearray,将它们明确地从不匹配序列模式。

如果我们将类似这些类型的类型定义为一个新的 collections.abc.AtomicSequence 抽象基类,那么这种特殊处理可能会被删除,这些类型在概念上是一个单一的项,但仍然实现序列协议以允许对其组成部分进行随机访问。

用于从实例中检索多个属性的表达式语法

实例属性模式句法已被设计成可以作为从单个表达式中检索对象多个属性的通用句法的基础。

host, port = obj{.host, .port}

与切片句法只允许在下标内一样,用于命名属性的 .attr 句法只允许在花括号下标内。

这个想法对于模式匹配的有用性不是必需的,因此它不是本 PEP 的一部分。然而,它被提及为使模式匹配感觉与语言的其余部分更集成的一种可能途径,而不是永远存在于其完全分离的世界中。

用于从实例中检索多个属性的表达式语法

如果花括号下标句法被接受用于实例属性模式匹配,并且随后扩展为提供通用的多个属性提取,那么它可以进一步扩展,允许从容器中检索多个项,这取决于用于映射模式匹配的句法。

host, port = obj{"host", "port"}
first, last = obj{0, -1}

同样,这个想法对于模式匹配的有用性不是必需的,因此它不是本 PEP 的一部分。然而,与检索多个属性一样,它被包含为拟议的模式匹配句法启发通用对象解构思想的示例。

被拒绝的想法

限制在值约束和映射模式键中允许的表达式

虽然在技术上完全可能将允许在值约束和映射模式键中使用的表达式类型限制为仅属性查找和常量字面量(如 PEP 634 所做),但这样做并没有明确的运行时价值,因此本 PEP 建议允许任何类型的基本表达式(基本表达式是语法中现有的节点类型,包括字面量、名称、属性查找、函数调用、容器下标、括号分组等),以及高优先级的一元运算符(+-~)在基本表达式上。

虽然 PEP 635 多次强调字面量模式和值模式不是完整表达式,但它从未阐述过从该限制中获得的具体好处(仅仅是理论上吸引人认为区分静态检查和动态检查很有用,而代码样式工具仍然可以强制执行这一点,即使编译器本身更宽松)。

上次我们施加此类限制是针对装饰器表达式,其主要结果是用户不得不忍受多年的笨拙的句法变通方法(例如将任意表达式嵌套在仅返回其参数的函数调用中),直到语言定义最终更新为允许任意表达式并让用户自己决定可读性。

PEP 634 中与装饰器表达式情况相似的情况是,任意表达式在技术上支持值模式,它们只是需要笨拙的变通方法,其中所有要匹配的值都需要在一个助手类中指定,该类位于 match 语句之前。

# Allowing arbitrary match targets with PEP 634's value pattern syntax
class mt:
    value = func()
match expr:
    case (_, mt.value):
        ... # Handle the case where 'expr[1] == func()'

或者,它们需要写成捕获模式和守卫表达式的组合。

# Allowing arbitrary match targets with PEP 634's guard expressions
match expr:
    case (_, _matched) if _matched == func():
        ... # Handle the case where 'expr[1] == func()'

本 PEP 建议跳过要求任何此类变通方法,而是从一开始就支持任意值约束。

match expr:
    case (__, == func()):
        ... # Handle the case where 'expr == func()'

是否实际编写此类代码是一个风格指南和代码 linters 的主题,而不是语言编译器。

特别是,如果静态分析器无法遵循某些动态检查,那么它们可以在分析时限制允许的表达式,而不是在编译时由编译器限制它们。

还有一些表达式几乎肯定会产生无意义的结果(例如 yieldyield fromawait),这是由于模式缓存规则,约束表达式实际求值的次数将取决于实现。即使在这种情况下,PEP 也采取了让用户编写无意义内容的观点,如果他们真的想的话。

除了最近更新的装饰器表达式外,Python 的正式句法在实践中几乎从不使用的另一个提供完全表达自由的场景是在 except 子句中:要匹配的异常几乎总是采用简单名称、点名称或这些名称的元组的形式,但语言语法允许在此处使用任意表达式。这很好地表明 Python 的用户基础可以信任他们自己负责找到可读的方式来使用宽松的语言特性,通过避免编写难以阅读的结构,即使它们被编译器允许。

这种宽松带来了切实的实现方面的好处:编译器中数十行的特定于 match 语句的代码被替换为对现有表达式编译代码的简单调用(包括在 AST 验证过程、AST 优化过程、符号表分析过程和代码生成过程中)。这种实现上的好处不仅会惠及 CPython,还会惠及每一个希望添加 match 语句支持的其他 Python 实现。

要求使用约束前缀标记来表示映射模式的键

本提案的第一个(未发布)草案建议要求映射模式键是值约束,就像 PEP 634 要求它们是有效的字面量或值模式一样。

import constants

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

然而,额外的字符在句法上很嘈杂,与它们在值约束中的使用(区分它们与非模式表达式)不同,这里的名称前缀没有提供任何超出表达式作为映射模式中键的位置所传达的额外信息。

因此,提案被简化为从映射模式键中省略了标记前缀。

此省略也符合容器可能将同一性和相等性检查都纳入其查找过程的事实;它们不纯粹依赖于相等性检查,而使用相等性约束前缀会错误地暗示这一点。

允许省略映射值约束的键/值分隔符

实例属性模式允许在编写属性值约束时省略 : 分隔符,如 case object{.attr == expr}

考虑为映射值约束提供类似的简写,但允许它会产生令人费解的结构,如 case {0 == 0}:,其中编译器知道这是键 0,值为约束 == 0,但人类读者看到的是同义的比较操作 0 == 0。包含键/值分隔符后,意图对人类读者也更明显:case {0: == 0}:

参考实现

本 PEP 的一个草案参考实现 [3] 源自 Brandt Bucher 为 PEP 634 [4] 所做的参考实现。

相对于本 PEP 的文本,草案参考实现尚未将 MATCH_CLASS 中对几个内置和标准库类型的特殊处理与 __match_args__ 设置为 None 的更通用检查相配合。类定义的模式目前仍然接受不定义 __match_args__ 的类。

所有其他修改后的模式都已更新为遵循本 PEP 而不是 PEP 634

match 模式的未解析尚未迁移到更新的 v3 AST。

match 模式的 AST 验证器尚未实现。

AST 验证器总体上尚未进行审查,以确保它正在检查只有表达式节点在期望表达式节点的地方才被传递。

本 PEP 中的示例尚未转换为测试用例,因此可能包含拼写错误和其他错误。

一些旧的 PEP 634 测试仍有待转换为新的 SyntaxError 测试。

文档尚未更新。

致谢

PEP 622PEP 634/PEP 635/PEP 636 的作者,因为本 PEP 中的提案仅仅是试图通过建议从更明确的句法开始,并可能稍后为特别常见的操作引入句法快捷方式,来改进一个已经精心构建的构想的可读性,这比尝试定义快捷方式版本要好。对于两个 PEP 相同(或至少非常相似)的规范领域,本 PEP 中描述预期行为的文本通常直接来自 PEP 634 文本。

Steven D’Aprano,他提出了一个有说服力的论点,即本 PEP 的关键目标可以通过使用现有的比较标记来实现,这些标记告诉编译器可以覆盖其“大多数用户在大多数时候最想要什么”的猜测,因为这些猜测不可避免地对至少一些用户在某些时候是错误的,并且保留 PEP 634 的一些句法糖(具有略有不同的语义定义)以在大多数情况下获得与 PEP 634 相同的简洁程度。(Paul Sokolosvsky 也独立建议使用 == 而不是 ? 作为相等性约束更容易理解的前缀)。

Thomas Wouters,他发布的 PEP 640 – 未使用的变量句法,以及对结构化模式匹配提案的公开审查,说服了本 PEP 的作者继续倡导通配符模式句法,该句法可以通过未来的 PEP 有可能变成一个硬关键字,该关键字始终跳过在任何期望简单名称的位置绑定引用,而不是像这里提议的那样无限期地作为特定于 match 模式的软关键字。

Joao Bueno 和 Jim Jewett 促使 PEP 作者仔细研究类模式和映射模式中子元素捕获的拟议句法(特别是“捕获到右侧”的问题)。这次审查促使了提案 v2 和 v3 之间的重大变更。

参考资料

附录 A – 完整语法

这里是 match_stmt 的完整修改后语法,取代了 PEP 634 中的附录 A。

标准 EBNF 之外使用的符号标记遵循 PEP 534

  • 'KWD' 表示硬关键字。
  • "KWD" 表示软关键字。
  • SEP.RULE+RULE (SEP RULE)* 的简写
  • !RULE 是否定先行断言
match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
subject_expr:
    | star_named_expression ',' [star_named_expressions]
    | named_expression
case_block: "case" (guarded_pattern | open_pattern) ':' block

guarded_pattern: closed_pattern 'if' named_expression
open_pattern: # Pattern may use multiple tokens with no closing delimiter
    | as_pattern
    | or_pattern

as_pattern: [closed_pattern] pattern_as_clause
as_pattern_with_inferred_wildcard: pattern_as_clause
pattern_as_clause: 'as' pattern_capture_target
pattern_capture_target: !"__" NAME !('.' | '(' | '=')

or_pattern: '|'.simple_pattern+

simple_pattern: # Subnode where "as" and "or" patterns must be parenthesised
    | closed_pattern
    | value_constraint

value_constraint:
    | eq_constraint
    | id_constraint

eq_constraint: '==' closed_expr
id_constraint: 'is' closed_expr

closed_expr: # Require a single token or a closing delimiter in expression
    | primary
    | closed_factor

closed_factor: # "factor" is the main grammar node for these unary ops
    | '+' primary
    | '-' primary
    | '~' primary

closed_pattern: # Require a single token or a closing delimiter in pattern
    | wildcard_pattern
    | group_pattern
    | structural_constraint

wildcard_pattern: "__"

group_pattern: '(' open_pattern ')'

structural_constraint:
    | sequence_constraint
    | mapping_constraint
    | attrs_constraint
    | class_constraint

sequence_constraint: '[' [sequence_constraint_elements] ']'
sequence_constraint_elements: ','.sequence_constraint_element+ ','?
sequence_constraint_element:
    | star_pattern
    | simple_pattern
    | as_pattern_with_inferred_wildcard
star_pattern: '*' (pattern_as_clause | wildcard_pattern)

mapping_constraint: '{' [mapping_constraint_elements] '}'
mapping_constraint_elements: ','.key_value_constraint+ ','?
key_value_constraint:
    | closed_expr pattern_as_clause
    | closed_expr ':' simple_pattern
    | double_star_capture
double_star_capture: '**' pattern_as_clause

attrs_constraint:
    | name_or_attr '{' [attrs_constraint_elements] '}'
name_or_attr: attr | NAME
attr: name_or_attr '.' NAME
attrs_constraint_elements: ','.attr_value_constraint+ ','?
attr_value_constraint:
    | '.' NAME pattern_as_clause
    | '.' NAME value_constraint
    | '.' NAME ':' simple_pattern
    | '.' NAME

class_constraint:
    | name_or_attr '(' ')'
    | name_or_attr '(' positional_patterns ','? ')'
    | name_or_attr '(' class_constraint_attrs ')'
    | name_or_attr '(' positional_patterns ',' class_constraint_attrs] ')'
positional_patterns: ','.positional_pattern+
positional_pattern:
    | simple_pattern
    | as_pattern_with_inferred_wildcard
class_constraint_attrs:
    | '**' '{' [attrs_constraint_elements] '}'

附录 B:抽象语法树更改摘要

以下新节点由本 PEP 添加到 AST 中:

stmt = ...
      | ...
      | Match(expr subject, match_case* cases)
      | ...
      ...

match_case = (pattern pattern, expr? guard, stmt* body)

pattern = MatchAlways
     | MatchValue(matchop op, expr value)
     | MatchSequence(pattern* patterns)
     | MatchMapping(expr* keys, pattern* patterns)
     | MatchAttrs(expr cls, identifier* attrs, pattern* patterns)
     | MatchClass(expr cls, pattern* patterns, identifier* extra_attrs, pattern* extra_patterns)

     | MatchRestOfSequence(identifier? target)
     -- A NULL entry in the MatchMapping key list handles capturing extra mapping keys

     | MatchAs(pattern? pattern, identifier target)
     | MatchOr(pattern* patterns)

      attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset)

matchop = EqCheck | IdCheck

附录 C:相对于 PEP 634 的更改摘要

整体 match/case 语句句法和守卫表达式句法与 PEP 634 中的保持相同。

PEP 634 相比,本 PEP 进行了以下关键更改:

  • 在 AST 中定义了一个新的 pattern 类型,而不是重用 expr 类型作为模式。
  • 新的 MatchAsMatchOr AST 节点从 expr 类型移到了 pattern 类型。
  • 通配符模式从 _(单个下划线)更改为 __(双下划线),并在 AST 中获得了专用的 MatchAlways 节点。
  • 由于意图不明,移除了值模式和字面量模式。
  • 引入了一个新的表达式类别:“封闭表达式”。
  • 封闭表达式是基本表达式,或者是前面有一个高优先级一元运算符(+-~)的封闭表达式。
  • 引入了一种新的模式类型:“值约束模式”。
  • 值约束有一个专用的 MatchValue AST 节点,而不是允许 Constant(字面量)、UnaryOp(负数)、BinOp(复数)和 Attribute(属性查找)的组合。
  • 值约束模式是相等性约束或同一性约束。
  • 相等性约束使用 == 作为前缀标记,后面跟着一个任意的封闭表达式:== EXPR
  • 同一性约束使用 is 作为前缀标记,后面跟着一个任意的封闭表达式:is EXPR
  • 由于意图不明,移除了捕获模式。所有捕获操作都使用 as 关键字(即使在序列匹配中),并在 AST 中表示为 MatchAsMatchRestOfSequence 节点。
  • 为了减少 AS 模式的冗长,允许 as NAME,其含义与 __ as NAME 相同。
  • 序列模式更改为要求使用方括号,而不是提供与赋值目标相同的句法灵活性(赋值语句允许通过任何元组分隔的目标的使用来指示可迭代解包,无论是否有围绕的括号或方括号)。
  • 序列模式获得了一个专用的 MatchSequence AST 节点,而不是重用 List
  • 映射模式更改为允许任意封闭表达式作为键。
  • 映射模式获得了一个专用的 MatchMapping AST 节点,而不是重用 Dict
  • 为了减少映射模式的冗长,KEY : __ as NAME 可以缩短为 KEY as NAME
  • 类模式不再使用单个关键字参数句法进行属性匹配。相反,它们使用双星号句法,以及一种变体的映射模式句法,其属性名称带有点前缀。
  • 类模式获得了一个专用的 MatchClass AST 节点,而不是重用 Call
  • 为了减少冗长,类属性匹配允许在模式以 ==isas 开头时省略 :
  • 类模式将设置为 None__match_args__ 的任何类视为接受单个位置模式,该模式与整个对象匹配(避免了 PEP 634 中所需的特殊处理)。
  • 类模式在与未定义 __match_args__ 的对象一起使用时引发 TypeError
  • 添加了用于鸭子类型的专用句法,使得 case cls{...}: 大致等同于 case cls(**{...}):,但会跳过对 __match_args__ 存在性的检查。此模式还有一个专用的 AST 节点,MatchAttrs

请注意,推迟字面量模式也使得推迟是否需要分词器中的“INUMBER”标记来处理虚数字面量的问题成为可能。没有它,解析器就无法区分复数与常数的其他二进制加减运算,因此像 PEP 634 这样的提案必须在后续编译步骤中进行工作来检查正确使用。

附录 D:此提案的更改历史

本提案的第一个已发布迭代大多遵循 PEP 634,但建议使用 ?EXPR 作为相等性约束,?is EXPR 作为同一性约束,而不是 PEP 634 的值模式和字面量模式。

第二个已发布迭代大多采用了 Steven D’Aprano 的反提案,该提案在许多情况下保留了 PEP 634 风格的推断约束,但也允许使用 == EXPR 进行显式相等性约束,和 is EXPR 进行显式同一性约束。

第三个已发布(当前)迭代完全放弃了推断模式,以试图解决与模式 case {key: NAME}:case cls(attr=NAME): 将绑定 NAME 的事实相关的担忧,尽管它出现在另一个子表达式的右侧而未使用 as 关键字。修改后的提案还消除了编写 case TARGET1 as TARGET2: 的可能性,该模式将绑定到两个给定名称。在这些更改中,最令人担忧的是 case cls(attr=TARGET_NAME):,因为它涉及 = 的使用,并且绑定目标在右侧,这与赋值语句、函数调用和函数签名声明中的情况完全相反。


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

最后修改:2025-02-01 08:59:27 GMT