Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 520 – 保留类属性定义顺序

作者:
Eric Snow <ericsnowcurrently at gmail.com>
状态:
最终
类型:
标准轨迹
创建时间:
2016年6月7日
Python 版本:
3.6
历史记录:
2016年6月7日、2016年6月11日、2016年6月20日、2016年6月24日
决议:
Python-Dev 消息

目录

注意

由于紧凑字典已在 3.6 中实现,因此 __definition_order__ 已被移除。cls.__dict__ 现在主要实现了相同的功能。

摘要

类定义语法本质上是有序的。因此,在其中定义的类属性是有序的。除了帮助提高可读性之外,这种顺序有时也很重要。如果它可以在类定义之外自动获取,那么属性顺序就可以在无需额外样板代码(例如元类或手动枚举属性顺序)的情况下使用。鉴于此信息已经存在,获取属性定义顺序是合理的预期。但是,目前 Python 不会保留类定义中的属性顺序。

这个 PEP 通过保留在类定义主体中引入属性的顺序来改变这一点。该顺序现在将保存在类的 __definition_order__ 属性中。这允许对原始定义顺序进行自省,例如由类装饰器。

此外,这个 PEP 要求默认类定义命名空间默认情况下是有序的(例如 OrderedDict)。长期存在的类命名空间 (__dict__) 将保持 dict

动机

类定义中的属性顺序可能对依赖名称顺序的工具有用。但是,如果没有自动获取定义顺序,那么这些工具必须对用户施加额外的要求。例如,使用此类工具可能需要您的类使用特定的元类。此类要求通常足以阻止使用该工具。

一些可以使用这个 PEP 的工具包括

  • 文档生成器
  • 测试框架
  • CLI 框架
  • Web 框架
  • 配置生成器
  • 数据序列化器
  • 枚举工厂(我的最初动机)

背景

当使用 class 语句定义类时,类主体将在命名空间内执行。目前,该命名空间默认为 dict。如果元类定义了 __prepare__(),那么调用它的结果将用于类定义命名空间。

执行完成后,定义命名空间将被复制到一个新的 dict 中。然后原始定义命名空间将被丢弃。新的副本将被存储为类的命名空间,并通过只读代理以 __dict__ 的形式公开。

类属性定义顺序由定义命名空间中名称的插入顺序表示。因此,我们可以通过将定义命名空间切换到有序映射(例如 collections.OrderedDict)来访问定义顺序。这可以通过元类和 __prepare__ 来实现,如上所述。事实上,这正是使用 __prepare__ 的最常见用例。

此时,要访问定义顺序,唯一缺少的东西是在丢弃定义命名空间之前将其存储在类上。同样,这可以通过元类来完成。但是,这意味着只有使用这种元类的类才能保留定义顺序。这有两个实际问题

首先,它需要使用元类。元类为代码引入了额外的复杂性,在某些情况下(例如冲突)是一个问题。因此,当机会出现时,减少对它们的需要是值得的。PEP 422PEP 487 详细讨论了这一点。我们通过对默认类定义命名空间使用有序映射(例如,至少对于 CPython 而言,使用 OrderedDict)拥有这样的机会,从而几乎消除了对 __prepare__() 的需要。

其次,只有选择使用基于 OrderedDict 的元类的类才能访问定义顺序。对于需要普遍访问定义顺序的情况,这存在问题。

规范

第一部分

  • 所有类都有一个 __definition_order__ 属性
  • __definition_order__ 是一个标识符元组(或 None
  • __definition_order__ 始终被设置
    1. 在类主体执行期间,将名称插入类定义命名空间的顺序存储在元组中
    2. 如果在类主体中定义了 __definition_order__,那么它必须是标识符元组或 None;任何其他值都将导致 TypeError
    3. 没有类定义的类(例如内置类)的 __definition_order__ 被设置为 None
    4. 对于 __prepare__() 返回了非 OrderedDict(或其子类)的类,其 __definition_order__ 被设置为 None(除了 #2 适用之处)

没有改变

  • dir() 不会依赖于 __definition_order__
  • 描述符和自定义 __getattribute__ 方法在处理 __definition_order__ 方面不受约束

第二部分

  • 默认类定义命名空间现在是一个有序映射(例如 OrderdDict
  • cls.__dict__ 不会改变,仍然是围绕 dict 的只读代理

请注意,具有有序 dict 的 Python 实现不需要进行任何更改。

以下代码演示了第一部分和第二部分的近似等效语义

class Meta(type):
    @classmethod
    def __prepare__(cls, *args, **kwargs):
        return OrderedDict()

class Spam(metaclass=Meta):
    ham = None
    eggs = 5
    __definition_order__ = tuple(locals())

为什么是元组?

使用元组反映了我们公开在类上定义属性的顺序这一事实。由于定义在 __definition_order__ 被设置时就已经完成,因此值的內容和顺序不会改变。因此,我们使用一种类型来传达这种不变性状态。

为什么不是只读属性?

有一些有效的论据支持将 __definition_order__ 设为只读属性(如 cls.__dict__)。最值得注意的是,只读属性传达了属性为“完整”的本质,这正是 __definition_order__ 的正确描述。由于它代表特定一次性事件(类定义主体执行)的状态,允许替换值会降低对属性对应于原始类主体的信心。此外,默认情况下不可变的方法通常有助于使数据更容易理解。

但是,在这种情况下,仍然没有充分的理由来反对 Python 中根深蒂固的先例。根据 Guido 的说法

I don't see why it needs to be a read-only attribute. There are
very few of those -- in general we let users play around with
things unless we have a hard reason to restrict assignment (e.g.
the interpreter's internal state could be compromised). I don't
see such a hard reason here.

另外,请注意,可写 __definition_order__ 允许动态创建的类(例如,由 Cython 创建)仍然具有正确设置的 __definition_order__。这当然可以通过特定的类创建工具(例如 type() 或 C-API)来处理,而无需失去只读属性的语义。但是,对于可写属性而言,这是一个无关紧要的问题。

为什么不是“__attribute_order__”?

__definition_order__ 以类定义主体为中心。处理类命名空间 (__dict__) 的定义后用例是另一回事。对于侧重于类定义之外内容的功能而言,__definition_order__ 将是一个极具误导性的名称。

为什么不忽略“双下划线”名称?

以“__”开头和结尾的名称是为解释器保留的。实际上,它们与 __definition_order__ 的用户无关。相反,对于几乎所有人来说,它们只会造成混乱,导致大多数人做额外的工作(过滤掉双下划线名称)。在双下划线名称有意义的情况下,类定义可以手动设置 __definition_order__,从而简化常见情况。

但是,将双下划线名称排除在 __definition_order__ 之外意味着它们在定义顺序中的位置将无法挽回地丢失。默认情况下删除双下划线名称可能会无意中导致使用双下划线名称非传统方式的类出现问题。在这种情况下,最好谨慎行事,保留类定义中的所有名称。这不是什么大问题,因为过滤掉双下划线名称很容易

(name for name in cls.__definition_order__
      if not (name.startswith('__') and name.endswith('__')))

事实上,在一些应用场景中,可能会有其他类似的过滤标准,例如忽略以“_”开头的任何名称,排除所有方法,或仅包含描述符。归根结底,双下划线名称并非特殊到需要特殊对待的程度。

请注意,一些双下划线名称 (__name____qualname__) 默认情况下由编译器注入。因此,即使它们并非严格意义上的类定义主体的一部分,它们也会被包含在内。

为什么是 None 而不是空元组?

添加 __definition_order__ 的一个关键目标是保留类定义中的信息,这些信息在该 PEP 之前已丢失。一个结果是,__definition_order__ 暗示了一个原始类定义。使用 None 允许我们清楚地区分没有定义顺序的类。一个空元组清楚地表明一个类来自一个定义语句,但没有在其中定义任何属性。

为什么是 None 而不是不设置属性?

属性的缺失需要比 None__definition_order__ 的使用者更复杂的处理。

为什么限制手动设置的值?

如果在类体中手动设置 __definition_order__,则将使用它。我们要求它是一个标识符元组(或 None),以便 __definition_order__ 的使用者对该值有一个一致的期望。这有助于最大限度地提高该功能的实用性。

我们还可以允许一个任意可迭代对象用于手动设置的 __definition_order__ 并将其转换为元组。但是,并非所有可迭代对象都推断出定义顺序(例如 set)。因此,我们选择要求一个元组。

为什么不在非类型对象上隐藏 __definition_order__ ?

Python 在对类实例进行查找时,并没有付出太多努力来隐藏类特定的属性。虽然将 __definition_order__ 视为仅限类的属性可能很有意义,但在对象查找期间隐藏,但这超出了该 PEP 的目标。

关于 __slots__ ?

__slots__ 将被添加到 __definition_order__ 中,就像类定义体中的任何其他名称一样。实际的插槽名称将不会被添加到 __definition_order__ 中,因为它们不是在定义命名空间中设置为名称。

为什么 __definition_order__ 甚至有必要?

由于定义顺序在 __dict__ 中没有保留,因此在类定义执行完成后它就会丢失。类 *可以* 显式地将属性设置为主体中的最后一项。但是,然后独立的装饰器只能使用已执行此操作的类。相反,__definition_order__ 保留了来自类体中的这一位信息,以便它可以普遍使用。

对 C-API 类型支持

可以说,大多数由 C 定义的 Python 类型(例如内置类型、扩展模块)都有一个大致等效的概念,即定义顺序。因此,可以想象,__definition_order__ 可以自动设置为这些类型。该 PEP 没有引入任何此类支持。但是,它也没有禁止它。但是,由于 __definition_order__ 可以通过正常的属性赋值随时设置,因此它不需要在 C-API 中进行任何特殊处理。

具体情况

  • 内置类型
  • PyType_Ready
  • PyType_FromSpec

兼容性

该 PEP 不会破坏向后兼容性,除非有人严格依赖 dict 作为类定义命名空间。这应该不会成为问题,因为 issubclass(OrderedDict, dict) 为真。

更改

除了类语法之外,以下还公开了新行为

  • builtins.__build_class__
  • types.prepare_class
  • types.new_class

此外,builtins.type() 的 3 个参数形式将允许将 __definition_order__ 包含到传递的命名空间中。它将受到与显式地在类体中定义 __definition_order__ 时相同的约束。

其他 Python 实现

在等待反馈的情况下,对 Python 实现的影响预计将很小。所有符合规范的实现都应该按照本 PEP 中的描述设置 __definition_order__

实现

该实现可以在 跟踪器 中找到。

备选方案

一个保持顺序的 cls.__dict__

而不是将定义顺序存储在 __definition_order__ 中,现在已排序的定义命名空间可以被复制到一个新的 OrderedDict 中。然后,它将被用作作为 __dict__ 代理的映射。

但是,将 OrderedDict 用于 __dict__ 会模糊与定义命名空间的关系,使其不太有用。

此外,(在 OrderedDict 的情况下),执行此操作将需要对具体 dict C-API 的语义进行重大更改。

已经有一些关于迁移到紧凑的字典实现的讨论,这将(大部分)保留插入顺序。但是,缺乏显式 __definition_order__ 仍然是一个痛点。

一个用于类定义的“命名空间”关键字参数

PEP 422 引入了一个新的“命名空间”关键字参数到类定义中,它有效地取代了对 __prepare__() 的需求。但是,该提议被撤回,转而支持更简单的 PEP 487

一个使用 OrderedDict 实现 __prepare__() 的标准库元类

这与编写自己的元类存在相同的问题。唯一的优势是您不必实际编写此元类。因此,它在该 PEP 的背景下没有提供任何好处。

在编译时设置 __definition_order__

每个类的 __qualname__ 在编译时确定。相同的概念可以应用于 __definition_order__。在编译时组合 __definition_order__ 的结果几乎与在运行时执行相同。

除了比较实现难度之外,关键区别在于,在编译时,保存动态在类体中设置的属性(例如 locals()[name] = value)的定义顺序将不切实际。但是,它们仍应反映在定义顺序中。一个可能的解决方案是要求类作者在动态定义任何类属性时手动设置 __definition_order__

最终,在运行时使用 OrderedDict 或在编译时发现几乎完全是一个实现细节。

参考资料


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

最后修改时间:2023-10-11 12:05:51 GMT