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 422 和 PEP 487 详细讨论了这一点。通过对默认类定义命名空间使用有序映射(例如 CPython 至少使用 OrderedDict),我们获得了这样的机会,几乎消除了对 __prepare__() 的需求。
其次,只有选择使用基于 OrderedDict 的元类的类才能访问定义顺序。这对于需要普遍访问定义顺序的情况来说是有问题的。
规范
第 1 部分
- 所有类都有一个
__definition_order__属性 __definition_order__是一个标识符的tuple(或None)__definition_order__始终设置- 在类主体执行期间,名称插入类 *定义* 命名空间的顺序存储在一个元组中
- 如果
__definition_order__在类主体中定义,则它必须是一个标识符的tuple或None;任何其他值都将导致TypeError - 没有类定义的类(例如内置类型)的
__definition_order__设置为None - 对于
__prepare__()返回的不是OrderedDict(或其子类)的类,其__definition_order__设置为None(#2 适用的情况除外)
不改变
dir()不会依赖于__definition_order__- 描述符和自定义
__getattribute__方法不受__definition_order__限制
第 2 部分
- 默认的类 *定义* 命名空间现在是一个有序映射(例如
OrderedDict) cls.__dict__不变,仍然是dict的只读代理
请注意,具有有序 dict 的 Python 实现无需进行任何更改。
以下代码展示了第 1 部分和第 2 部分大致等效的语义
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() 的三参数形式将允许在传入的命名空间中包含 __definition_order__。它将受到与 __definition_order__ 在类主体中显式定义时相同的约束。
其他 Python 实现
根据反馈,对 Python 实现的影响预计将很小。所有符合规范的实现都应按照本 PEP 的描述设置 __definition_order__。
实施
实现可在 跟踪器 中找到。
备选方案
一个保留顺序的 cls.__dict__
与其将定义顺序存储在 __definition_order__ 中,不如将现在有序的定义命名空间复制到一个新的 OrderedDict 中。然后将其用作代理为 __dict__ 的映射。这样做将基本提供相同的语义。
然而,将 OrderedDict 用于 __dict__ 会模糊与定义命名空间的关系,使其用处降低。
此外,(特别是对于 OrderedDict) 这样做将需要对具体的 dict C-API 的语义进行重大更改。
曾有一些关于转向紧凑字典实现(它将(大部分)保留插入顺序)的讨论。然而,缺乏明确的 __definition_order__ 仍然是一个痛点。
类定义中的“namespace”关键字参数
PEP 422 在类定义中引入了一个新的“namespace”关键字参数,有效地取代了对 __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