如何使数据类更好地与 __slots__ 一起工作?

新手上路,请多包涵

决定 从 Python 3.7 的数据类中删除对 __slots__ 的直接支持。

尽管如此, __slots__ 仍然可以与数据类一起使用:

 from dataclasses import dataclass

@dataclass
class C():
    __slots__ = "x"
    x: int

但是,由于 __slots__ 的工作方式,无法为数据类字段分配默认值:

 from dataclasses import dataclass

@dataclass
class C():
    __slots__ = "x"
    x: int = 1

这会导致错误:

 Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 'x' in __slots__ conflicts with class variable

__slots__ 和默认 dataclass 字段如何一起工作?

原文由 Rick 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.1k
2 个回答

2021 年更新:对 __slots__ 的直接支持已 添加到 python 3.10 中。我将这个答案留给后代,不会对其进行更新。

这个问题并不是数据类独有的。任何冲突的类属性都会踩到一个槽:

 >>> class Failure:
...     __slots__ = tuple("xyz")
...     x=1
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 'x' in __slots__ conflicts with class variable

这就是插槽的工作原理。发生错误是因为 __slots__ 为每个插槽名称创建了一个类级描述符对象:

 >>> class Success:
...     __slots__ = tuple("xyz")
...
>>>
>>> type(Success.x)
<class 'member_descriptor'>

为了防止这种冲突的变量名错误,必须在实例化类对象 之前 更改类命名空间,以便在类中没有两个对象竞争相同的成员名称:

  • 指定的(默认)值*
  • 插槽描述符(由插槽机制创建)

出于这个原因,父类上的 __init_subclass__ 方法是不够的,类装饰器也不够,因为在这两种情况下,类对象已经在这些函数收到要更改的类时创建它。

当前选项:写一个元类

直到插槽机制被更改以允许更大的灵活性,或者语言本身提供了在实例化类对象之前更改类名称空间的机会之前,我们唯一的选择是使用元类。

为解决此问题而编写的任何元类至少必须:

  • 从命名空间中删除冲突的类属性/成员
  • 实例化类对象以创建槽描述符
  • 保存对槽描述符的引用
  • 将先前删除的成员及其值放回类中 __dict__ (这样 dataclass 机器可以找到它们)
  • 将类对象传递给 dataclass 装饰器
  • 将插槽描述符恢复到各自的位置
  • 还要考虑很多极端情况(例如,如果有 __dict__ 槽该怎么办)

至少可以说,这是一项极其复杂的工作。像下面这样定义类会更容易——没有默认值,这样根本不会发生冲突——然后再添加一个默认值。

当前选项:在类对象实例化后进行更改

未更改的数据类如下所示:

 @dataclass
class C:
    __slots__ = "x"
    x: int

改动很简单。更改 __init__ 签名以反映所需的默认值,然后更改 __dataclass_fields__ 以反映默认值的存在。

 from functools import wraps

def change_init_signature(init):
    @wraps(init)
    def __init__(self, x=1):
        init(self,x)
    return __init__

C.__init__ = change_init_signature(C.__init__)

C.__dataclass_fields__["x"].default = 1

测试:

 >>> C()
C(x=1)
>>> C(2)
C(x=2)
>>> C.x
<member 'x' of 'C' objects>
>>> vars(C())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute

有用!

当前选项:a setmember 装饰器

通过一些努力,可以使用所谓的 setmember 装饰器以上述方式自动更改类。这将需要偏离数据类 API,以便在类主体以外的位置定义默认值,可能类似于:

 @setmember(x=field(default=1))
@dataclass
class C:
    __slots__="x"
    x: int

同样的事情也可以通过父类上的 __init_subclass__ 方法来完成:

 class SlottedDataclass:
    def __init_subclass__(cls, **kwargs):
        cls.__init_subclass__()
        # make the class changes here

class C(SlottedDataclass, x=field(default=1)):
    __slots__ = "x"
    x: int

未来的可能性:改变老虎机

如上所述,另一种可能性是 python 语言改变插槽机制以允许更大的灵活性。这样做的一种方法可能是更改槽描述符本身以在类定义时存储类级别数据。

也许,这可以通过提供 dict 作为 __slots__ 参数来完成(见下文)。类级数据(1 代表 x,2 代表 y)可以只存储在描述符本身上以供以后检索:

 class C:
    __slots__ = {"x": 1, "y": 2}

assert C.x.value == 1
assert C.y.value == y

一个困难:可能希望只有 slot_member.value 出现在某些插槽上,而不是其他插槽上。这可以通过从新的 slottools 库中导入一个空槽工厂来解决:

 from slottools import nullslot

class C:
    __slots__ = {"x": 1, "y": 2, "z": nullslot()}

assert not hasattr(C.z, "value")

上面建议的代码风格与数据类 API 有所不同。然而,插槽机制本身甚至可以被修改以允许这种代码风格,特别是考虑到数据类 API 的适应性:

 class C:
    __slots__ = "x", "y", "z"
    x = 1  # 1 is stored on C.x.value
    y = 2  # 2 is stored on C.y.value

assert C.x.value == 1
assert C.y.value == y
assert not hasattr(C.z, "value")

未来的可能性:在类体内“准备”类命名空间

另一种可能性是改变/准备(元类的 __prepare__ 方法的同义词)类命名空间。

目前,没有机会(除了编写元类)在类对象实例化之前编写更改类名称空间的代码,并且槽机制开始工作。这可以通过创建一个用于预先准备类名称空间的挂钩来改变,并使其仅在该挂钩运行后才会产生抱怨名称冲突的错误。

这个所谓的 __prepare_slots__ 钩子可能看起来像这样,我认为这还不错:

 from dataclasses import dataclass, prepare_slots

@dataclass
class C:
    __slots__ = ('x',)
    __prepare_slots__ = prepare_slots
    x: int = field(default=1)

dataclasses.prepare_slots 函数只是一个函数--- 类似于 __prepare__ 方法— 接收类命名空间并在创建类之前更改它。特别是对于这种情况,默认数据类字段值将存储在其他一些方便的地方,以便在创建槽描述符对象后可以检索它们。


\* 请注意,如果正在使用 dataclasses.field ,则与插槽冲突的默认字段值也可能由数据类机器创建。

原文由 Rick 发布,翻译遵循 CC BY-SA 4.0 许可协议

在 Python 3.10+ 中,您可以使用 slots=Truedataclass 以提高内存效率:

 from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Point:
    x: int = 0
    y: int = 0

这样您也可以设置默认字段值。

原文由 Eugene Yarmash 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题