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

Python 增强提案

PEP 653 – 精确模式匹配语义

作者:
Mark Shannon <mark at hotpy.org>
状态:
草案
类型:
标准跟踪
创建日期:
2021年2月9日
发布历史:
2021年2月18日

目录

摘要

本PEP提出了一种模式匹配的语义,它尊重PEP 634的总体概念,但更加精确,更易于理解,并且应该更快。

除了PEP 634中的__match_args__属性之外,对象模型还将扩展两个特殊的(双下划线)属性,__match_container____match_class__,以支持模式匹配。这两个新属性都必须是整数,并且__match_args__必须是包含唯一字符串的元组。

通过本PEP

  • 模式匹配的语义将更加清晰,因此模式更容易理解。
  • 将能够以更高效的方式实现模式匹配。
  • 通过允许类对它们匹配的模式有更多控制,模式匹配将更适用于复杂类。

动机

PEP 634所述,Python中的模式匹配将被添加到Python 3.10中。不幸的是,PEP 634的语义不够精确,也不允许类充分控制它们如何匹配模式。

精确语义

PEP 634明确包含了一个关于未定义行为的部分。大量的未定义行为在C语言这样的语言中可能是可接受的,但在Python中应该尽量减少。Python中的模式匹配可以更精确地定义,而不会失去表达能力或性能。

增强类匹配的控制

PEP 634将类是序列还是映射的决定委托给collections.abc。并非所有可以被视为序列的类都注册为collections.abc.Sequence的子类。本PEP允许它们匹配序列模式,而无需完整的collections.abc.Sequence机制。

PEP 634赋予了一些内置类一种特殊的匹配形式,即“自身”匹配。例如,模式list(x)匹配一个列表并将该列表赋给x。通过允许类选择它们匹配的模式类型,其他类也可以使用这种形式。

例如,使用sympy,我们可能希望编写

# a*a == a**2
case Mul(args=[Symbol(a), Symbol(b)]) if a == b:
    return Pow(a, 2)

这要求sympy类Symbol进行“自身”匹配。对于sympy来说,使用PEP 634支持这种模式是可能的,但有点棘手。有了本PEP,它可以很容易地实现[1]

健壮性

有了这个PEP,模式匹配期间对属性的访问变得明确且确定。这使得在匹配具有隐藏副作用的对象(例如对象关系映射器)时,模式匹配不易出错。对象将对其自身的解构拥有更多控制权,这有助于防止属性访问产生副作用时出现意外结果。

PEP 634在确定一个值可以匹配哪些模式时依赖于collections.abc模块,如果需要会隐式导入它。本PEP将消除这些导入带来的令人惊讶的导入错误和误导性审计事件。

高效实现

本PEP中提出的语义将允许高效实现,部分原因是其精确的语义,部分原因是使用了对象模型。

通过精确的语义,可以推断出哪些代码转换是正确的,从而有效地应用优化。

因为对象模型是Python的核心部分,实现已经高效地处理了特殊属性查找。查找特殊属性比在抽象基类上执行子类测试快得多。

基本原理

对象模型和特殊方法是Python语言的核心。因此,实现对其支持良好。使用特殊属性进行模式匹配,使得模式匹配能够以与实现的其他部分良好集成的方式实现,从而更容易维护,并且可能表现更好。

一个匹配语句执行一系列模式匹配。通常,匹配一个模式有三个部分:

  1. 该值能否匹配这种模式?
  2. 当解构时,该值是否匹配这个特定模式?
  3. 守卫是否为真?

为了确定一个值能否匹配特定类型的模式,我们添加了__match_container____match_class__属性。这允许以高效的方式确定值的类型。

规范

对象模型的补充

__match_container____match_class__ 属性将被添加到 object 中。__match_container__ 应该由希望匹配映射或序列模式的类重写。__match_class__ 应该由希望更改匹配类模式时默认行为的类重写。

__match_container__ 必须是一个整数,且应严格为以下值之一

0
MATCH_SEQUENCE = 1
MATCH_MAPPING = 2

MATCH_SEQUENCE 用于指示该类的实例可以匹配序列模式。

MATCH_MAPPING 用于指示该类的实例可以匹配映射模式。

__match_class__ 必须是一个整数,且应严格为以下值之一

0
MATCH_SELF = 8

MATCH_SELF 用于指示,对于单个位置参数类模式,将使用主题而不进行解构。

注意

在本文档的其余部分,我们将仅通过名称来指代上述值。Python和C都将提供符号常量,并且这些值将永远不会改变。

object 的特殊属性值如下

__match_container__ = 0
__match_class__= 0
__match_args__ = ()

这些特殊属性将正常继承。

如果__match_args__被重写,则它必须包含一个由唯一字符串组成的元组。它可以为空。

注意

__match_args__将按照PEP 634的规定,为数据类和命名元组自动生成。

模式匹配实现**不**要求检查任何这些属性是否按指定行为。如果__match_container____match_class____match_args__的值未按指定,则实现可能引发任何异常,或匹配错误的模式。当然,如果实现能够高效地进行检查,则可以自由检查这些属性并提供有意义的错误消息。

匹配过程的语义

在下文中,所有形式为 $var 的变量都是临时变量,对 Python 程序不可见。它们可能通过内省可见,但这属于实现细节,不应依赖。伪语句 FAIL 用于表示该模式匹配失败,应继续匹配下一个模式。如果控制流在未遇到 FAIL 的情况下到达翻译结束,则表示已匹配成功,后续模式将被忽略。

形式为 $ALL_CAPS 的变量是持有语法元素的元变量,它们不是普通变量。因此,$VARS = $items 并不是将 $items 赋值给 $VARS,而是将 $items 解包到 $VARS 所持有的变量中。例如,对于抽象语法 case [$VARS]: 和具体语法 case[a, b]:,那么 $VARS 将持有变量 (a, b),而不是这些变量的值。

伪函数QUOTE接受一个变量并返回该变量的名称。例如,如果元变量$VAR持有变量foo,那么QUOTE($VAR) == "foo"

下面列出的所有不在原始源中存在的额外代码都不会触发行事件,符合PEP 626

前言

在匹配任何模式之前,对被匹配的表达式进行评估

match expr:

转换为

$value = expr

捕获模式

捕获模式总是匹配,因此是不可驳的匹配

case capture_var:

转换为

capture_var = $value

通配符模式

通配符模式总是匹配,所以

case _:

转换为

# No code -- Automatically matches

字面量模式

字面量模式

case LITERAL:

转换为

if $value != LITERAL:
    FAIL

除非字面量是 NoneTrueFalse 之一,否则它将转换为

if $value is not LITERAL:
    FAIL

值模式

值模式

case value.pattern:

转换为

if $value != value.pattern:
    FAIL

序列模式

不包含星号模式的模式

case [$VARS]:

转换为

$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
    FAIL
if len($value) != len($VARS):
    FAIL
$VARS = $value

示例:[2]

包含星号模式的模式

case [$VARS]

转换为

$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
    FAIL
if len($value) < len($VARS):
    FAIL
$VARS = $value # Note that $VARS includes a star expression.

示例:[3]

映射模式

不包含双星号模式的模式

case {$KEYWORD_PATTERNS}:

转换为

$sentinel = object()
$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
    FAIL
# $KEYWORD_PATTERNS is a meta-variable mapping names to variables.
for $KEYWORD in $KEYWORD_PATTERNS:
    $tmp = $value.get(QUOTE($KEYWORD), $sentinel)
    if $tmp is $sentinel:
        FAIL
    $KEYWORD_PATTERNS[$KEYWORD] = $tmp

示例:[4]

包含双星号模式的模式

case {$KEYWORD_PATTERNS, **$DOUBLE_STARRED_PATTERN}:

转换为

$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
    FAIL
# $KEYWORD_PATTERNS is a meta-variable mapping names to variables.
$tmp = dict($value)
if not $tmp.keys() >= $KEYWORD_PATTERNS.keys():
    FAIL:
for $KEYWORD in $KEYWORD_PATTERNS:
    $KEYWORD_PATTERNS[$KEYWORD] = $tmp.pop(QUOTE($KEYWORD))
$DOUBLE_STARRED_PATTERN = $tmp

示例:[5]

类模式

没有参数的类模式

case ClsName():

转换为

if not isinstance($value, ClsName):
    FAIL

带单个位置模式的类模式

case ClsName($VAR):

转换为

$kind = type($value).__match_class__
if $kind == MATCH_SELF:
    if not isinstance($value, ClsName):
        FAIL
    $VAR = $value
else:
    As other positional-only class pattern

仅位置类模式

case ClsName($VARS):

转换为

if not isinstance($value, ClsName):
    FAIL
$attrs = ClsName.__match_args__
if len($attr) < len($VARS):
    raise TypeError(...)
try:
    for i, $VAR in enumerate($VARS):
        $VAR = getattr($value, $attrs[i])
except AttributeError:
    FAIL

示例:[6]

所有关键字模式的类模式

case ClsName($KEYWORD_PATTERNS):

转换为

if not isinstance($value, ClsName):
    FAIL
try:
    for $KEYWORD in $KEYWORD_PATTERNS:
        $tmp = getattr($value, QUOTE($KEYWORD))
        $KEYWORD_PATTERNS[$KEYWORD] = $tmp
except AttributeError:
    FAIL

示例:[7]

带位置和关键字模式的类模式

case ClsName($VARS, $KEYWORD_PATTERNS):

转换为

if not isinstance($value, ClsName):
    FAIL
$attrs = ClsName.__match_args__
if len($attr) < len($VARS):
    raise TypeError(...)
$pos_attrs = $attrs[:len($VARS)]
try:
    for i, $VAR in enumerate($VARS):
        $VAR = getattr($value, $attrs[i])
    for $KEYWORD in $KEYWORD_PATTERNS:
        $name = QUOTE($KEYWORD)
        if $name in pos_attrs:
            raise TypeError(...)
        $KEYWORD_PATTERNS[$KEYWORD] = getattr($value, $name)
except AttributeError:
    FAIL

示例:[8]

嵌套模式

上述规范假设模式不嵌套。对于嵌套模式,通过引入临时捕获模式递归地应用上述转换。

例如,模式

case [int(), str()]:

转换为

$kind = type($value).__match_class__
if $kind != MATCH_SEQUENCE:
    FAIL
if len($value) != 2:
    FAIL
$value_0, $value_1 = $value
#Now match on temporary values
if not isinstance($value_0, int):
    FAIL
if not isinstance($value_1, str):
    FAIL

守卫

守卫在翻译的其余部分之后转换为测试

case pattern if guard:

转换为

[translation for pattern]
if not guard:
    FAIL

不符合规范的特殊属性

所有类都应确保 __match_container____match_class____match_args__ 的值符合规范。因此,实现可以假定(无需检查)以下内容为真

__match_container__ == 0 or __match_container__ == MATCH_SEQUENCE or __match_container__ == MATCH_MAPPING
__match_class__ == 0 or __match_class__ == MATCH_SELF

并且 __match_args__ 是一个由唯一字符串组成的元组。

标准库中类的特殊属性值

对于核心内置容器类,__match_container__ 将是

  • listMATCH_SEQUENCE
  • tupleMATCH_SEQUENCE
  • dictMATCH_MAPPING
  • 字节数组: 0
  • 字节: 0
  • str: 0

命名元组的 __match_container__ 将设置为 MATCH_SEQUENCE

  • 所有其他标准库类,如果issubclass(cls, collections.abc.Mapping)为真,则其__match_container__将设置为MATCH_MAPPING
  • 所有其他标准库类,如果issubclass(cls, collections.abc.Sequence)为真,则其__match_container__将设置为MATCH_SEQUENCE

对于以下内置类,__match_class__ 将设置为 MATCH_SELF

  • 布尔值
  • 字节数组
  • 字节
  • 浮点数
  • 不可变集合
  • int
  • 集合
  • str
  • 列表
  • 元组
  • 字典

安全隐患

无。

实施

从规范得出的朴素实现效率不高。幸运的是,有一些相当直接的转换可以用来提高性能。性能应该与PEP 634的实现(在撰写本文时)在3.10版本发布时相当。进一步的性能改进可能要等到3.11版本发布。

可能进行的优化

以下内容不属于规范,而是帮助开发人员创建高效实现的指导原则。

将评估拆分为多个“通道”

由于匹配每个模式的第一步是检查其类型,因此可以将所有类型检查组合成一个多路分支,放在匹配的开头。然后可以将案例列表复制到多个“通道”中,每个通道对应一种类型。这样就可以很容易地从每个通道中移除不匹配的案例。根据类型,每个通道可以采用不同的优化策略。请注意,匹配子句的主体不需要复制,只需复制模式即可。

序列模式

这可能是最复杂且性能提升最大的优化。由于每个模式只能匹配一个长度范围,通常只有一个长度,因此测试序列可以重写为对序列的显式迭代,只尝试匹配适用于该序列长度的模式。

例如

case []:
    A
case [x]:
    B
case [x, y]:
    C
case other:
    D

大致可以编译为

  # Choose lane
  $i = iter($value)
  for $0 in $i:
      break
  else:
      A
      goto done
  for $1 in $i:
      break
  else:
      x = $0
      B
      goto done
  for $2 in $i:
      del $0, $1, $2
      break
  else:
      x = $0
      y = $1
      C
      goto done
  other = $value
  D
done:

映射模式

这里最好的策略可能是根据映射的大小和存在的键形成一个决策树。没有必要重复测试键的存在。例如

match obj:
    case {a:x, b:y}:
        W
    case {a:x, c:y}:
        X
    case {a:x, b:_, c:y}:
        Y
    case other:
        Z

如果检查情况 X 时键 "a" 不存在,则无需再次检查 Y。

映射通道大致可以这样实现

# Choose lane
if len($value) == 2:
    if "a" in $value:
        if "b" in $value:
            x = $value["a"]
            y = $value["b"]
            goto W
        if "c" in $value:
            x = $value["a"]
            y = $value["c"]
            goto X
elif len($value) == 3:
    if "a" in $value and "b" in $value:
        x = $value["a"]
        y = $value["c"]
        goto Y
other = $value
goto Z

本PEP与PEP 634的差异摘要

语义上的变化可以概括为:

  • 要求__match_args__是一个字符串的**元组**,而不仅仅是一个序列。这使得模式匹配更加健壮和可优化,因为__match_args__可以被假定为不可变的。
  • 选择可以匹配的容器模式类型使用 cls.__match_container__ 而不是 issubclass(cls, collections.abc.Mapping)issubclass(cls, collections.abc.Sequence)
  • 如果需要,允许类通过设置 __match_class__ = 0 来完全退出解构。
  • 匹配模式时的行为定义得更精确,但除此之外没有改变。

语法没有变化。PEP 636教程中给出的所有示例都应继续按现有方式工作。

被拒绝的想法

使用实例字典中的属性

本PEP的早期版本仅在使用__match_class__为默认值匹配类模式时,使用实例字典中的属性。目的是避免捕获绑定方法和其他合成属性。然而,这也意味着属性被忽略了。

对于这个类

class C:
    def __init__(self):
        self.a = "a"
    @property
    def p(self):
        ...
    def m(self):
        ...

理想情况下,我们会匹配属性“a”和“p”,但不匹配“m”。然而,没有通用的方法可以做到这一点,因此本PEP现在遵循PEP 634的语义。

在主体上查找 __match_args__,而不是模式上

本PEP的早期版本在主体类而非模式中指定的类上查找 __match_args__。此方案因以下几个原因被拒绝:

* Using the class specified in the pattern is more amenable to optimization and can offer better performance.
* Using the class specified in the pattern has the potential to provide better error reporting is some cases.
* Neither approach is perfect, both have odd corner cases. Keeping the status quo minimizes disruption.

__match_class____match_container__ 合并为一个值

本PEP的早期版本将 __match_class____match_container__ 合并为一个值 __match_kind__。使用单一值在性能方面略有优势,但很可能在重写类匹配行为时导致容器匹配发生意外更改,反之亦然。

延迟的想法

本PEP的最初版本包含了匹配类型MATCH_POSITIONAL和特殊方法__deconstruct__,这将允许类完全控制其匹配。这对于像sympy这样的库非常重要。

例如,使用sympy,我们可能希望编写

# sin(x)**2 + cos(x)**2 == 1
case Add(Pow(sin(a), 2), Pow(cos(b), 2)) if a == b:
    return 1

对于sympy来说,用当前的模式匹配支持位置模式是可能的,但很棘手。有了这些额外的功能,它可以很容易地实现[9]

这个想法将在3.11的未来PEP中提出。然而,对于3.10开发周期来说,进行这样的更改为时已晚。

拥有一个独立的值来拒绝所有类匹配

在本PEP的早期版本中,__match_class__有一个不同的值,允许类不匹配任何需要解构的类模式。然而,一旦引入MATCH_POSITIONAL,这将变得冗余,并且使极少数情况的规范复杂化。

代码示例

class Symbol:
    __match_class__ = MATCH_SELF

这个

case [a, b] if a is b:

转换为

$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
    FAIL
if len($value) != 2:
    FAIL
a, b = $value
if not a is b:
    FAIL

这个

case [a, *b, c]:

转换为

$kind = type($value).__match_container__
if $kind != MATCH_SEQUENCE:
    FAIL
if len($value) < 2:
    FAIL
a, *b, c = $value

这个

case {"x": x, "y": y} if x > 2:

转换为

$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
    FAIL
$tmp = $value.get("x", $sentinel)
if $tmp is $sentinel:
    FAIL
x = $tmp
$tmp = $value.get("y", $sentinel)
if $tmp is $sentinel:
    FAIL
y = $tmp
if not x > 2:
    FAIL

这个

case {"x": x, "y": y, **z}:

转换为

$kind = type($value).__match_container__
if $kind != MATCH_MAPPING:
    FAIL
$tmp = dict($value)
if not $tmp.keys() >= {"x", "y"}:
    FAIL
x = $tmp.pop("x")
y = $tmp.pop("y")
z = $tmp

这个

match ClsName(x, y):

转换为

if not isinstance($value, ClsName):
    FAIL
$attrs = ClsName.__match_args__
if len($attr) < 2:
    FAIL
try:
    x = getattr($value, $attrs[0])
    y = getattr($value, $attrs[1])
except AttributeError:
    FAIL

这个

match ClsName(a=x, b=y):

转换为

if not isinstance($value, ClsName):
    FAIL
try:
    x = $value.a
    y = $value.b
except AttributeError:
    FAIL

这个

match ClsName(x, a=y):

转换为

if not isinstance($value, ClsName):
    FAIL
$attrs = ClsName.__match_args__
if len($attr) < 1:
    raise TypeError(...)
$positional_names = $attrs[:1]
try:
    x = getattr($value, $attrs[0])
    if "a" in $positional_names:
        raise TypeError(...)
    y = $value.a
except AttributeError:
    FAIL
class Basic:
    __match_class__ = MATCH_POSITIONAL
    def __deconstruct__(self):
        return self._args

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

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