如何使__slots__更好地使用数据类?

Ric*_*ica 8 python slots python-3.x python-dataclasses

它决定除去直接支持__slots__从数据类为Python 3.7。

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

from dataclasses import dataclass

@dataclass
class C():
    __slots__ = "x"
    x: int
Run Code Online (Sandbox Code Playgroud)

但是,由于这种方式__slots__有效,因此无法为数据类字段分配默认值:

from dataclasses import dataclass

@dataclass
class C():
    __slots__ = "x"
    x: int = 1
Run Code Online (Sandbox Code Playgroud)

这会导致错误:

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

如何__slots__与默认dataclass字段一起使用?

Eug*_*ash 49

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

from dataclasses import dataclass

@dataclass(slots=True)
class Point:
    x: int = 0
    y: int = 0
Run Code Online (Sandbox Code Playgroud)

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


Ric*_*ica 26

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
Run Code Online (Sandbox Code Playgroud)

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

>>> class Success:
...     __slots__ = tuple("xyz")
...
>>>
>>> type(Success.x)
<class 'member_descriptor'>
Run Code Online (Sandbox Code Playgroud)

为了防止这个变量名冲突的错误,在类对象被实例化之前必须改变类的命名空间,这样在类中不会有两个对象竞争同一个成员名:

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

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

当前选项:编写元类

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

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

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

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

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

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

@dataclass
class C:
    __slots__ = "x"
    x: int
Run Code Online (Sandbox Code Playgroud)

更改很简单。更改__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
Run Code Online (Sandbox Code Playgroud)

测试:

>>> 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
Run Code Online (Sandbox Code Playgroud)

有用!

当前选项:setmember装饰器

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

@setmember(x=field(default=1))
@dataclass
class C:
    __slots__="x"
    x: int
Run Code Online (Sandbox Code Playgroud)

同样的事情也可以通过__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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

assert C.x.value == 1
assert C.y.value == y
Run Code Online (Sandbox Code Playgroud)

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

from slottools import nullslot

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

assert not hasattr(C.z, "value")
Run Code Online (Sandbox Code Playgroud)

上面建议的代码风格与 dataclasses 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")
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

from dataclasses import dataclass, prepare_slots

@dataclass
class C:
    __slots__ = ('x',)
    __prepare_slots__ = prepare_slots
    x: int = field(default=1)
Run Code Online (Sandbox Code Playgroud)

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


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


biq*_*les 9

正如答案中已经指出的那样,数据类中的数据类不能生成槽,原因很简单,必须在创建类之前定义槽。

事实上,数据类的PEP明确提到了这一点:

至少对于初始版本来说,是__slots__不会被支持的。__slots__需要在创建类时添加。在创建类之后调用数据类装饰器,因此为了添加__slots__装饰器必须创建一个新类 set__slots__并返回它。因为这种行为有些出人意料,数据类的初始版本将不支持自动设置__slots__.

我想使用插槽是因为我需要在另一个项目中初始化很多很多数据类实例。我最终编写了自己的数据类替代实现,它支持这一点,还有一些额外的功能:dataclassy。

dataclassy 使用元类方法,它具有许多优点——它支持装饰器继承,大大降低了代码复杂性,当然还有槽的生成。使用 dataclassy 可以实现以下功能:

from dataclassy import dataclass

@dataclass(slots=True)
class Pet:
    name: str
    age: int
    species: str
    fluffy: bool = True
Run Code Online (Sandbox Code Playgroud)

打印Pet.__slots__输出预期的{'name', 'age', 'species', 'fluffy'}, 实例没有__dict__属性,因此对象的整体内存占用较低。这些观察结果表明__slots__已成功生成并且是有效的。另外,正如所证明的,默认值工作得很好。


mcg*_*uip 5

最少参与的解决方案,我发现这个问题是要指定一个自定义__init__使用object.__setattr__到设定值。

@dataclass(init=False, frozen=True)
class MyDataClass(object):
    __slots__ = (
        "required",
        "defaulted",
    )
    required: object
    defaulted: Optional[object]

    def __init__(
        self,
        required: object,
        defaulted: Optional[object] = None,
    ) -> None:
        super().__init__()
        object.__setattr__(self, "required", required)
        object.__setattr__(self, "defaulted", defaulted)

Run Code Online (Sandbox Code Playgroud)

  • 最糟糕的是,最简单的方法需要自己编写 init,如果最大的获胜数据类带来了桌面,那么这将是一个失败的方法! (2认同)
  • 是的,我绝对同意。除此之外,您将属性名称指定为字符串,这很容易被 ide 忽略。所有现有的解决方案在某种程度上看起来都很丑陋/容易出错...... (2认同)

TG-*_*hie 5

另一个解决方案是根据类型化注释在类主体内生成插槽参数。这看起来像:

@dataclass
class Client:
    first: str
    last: str
    age_of_signup: int
    
     __slots__ = slots(__annotations__)
Run Code Online (Sandbox Code Playgroud)

其中slots函数是:

def slots(anotes: Dict[str, object]) -> FrozenSet[str]:
    return frozenset(anotes.keys())
Run Code Online (Sandbox Code Playgroud)

运行会生成一个如下所示的 slot参数:frozenset({'first', 'last', 'age_of_signup})

这将采用其上方的注释并生成一组指定的名称。这里的限制是您必须为每个类重新键入该行__slots__ = slots(__annotations__),并且它必须位于所有注释的下方,并且它不适用于具有默认参数的注释。这还有一个优点,即 slot 参数永远不会与指定的注释冲突,因此您可以随意添加或删除成员,而不必担心维护单独的列表。

  • 但为什么将其转换为“frozenset”而不是仅仅使用“__annotations__.keys()”呢? (2认同)