python-3.x 如何使数据类更好地与__slots__配合使用?

5t7ly7z5  于 2023-03-20  发布在  Python
关注(0)|答案(6)|浏览(168)

was decided删除了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字段一起工作?

8qgya5xd

8qgya5xd1#

2021年更新:__slots__的直接支持是added to 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

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

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

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

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

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

当前选项:编写元类

除非插槽机制被修改以允许更大的灵活性,或者语言本身提供了在类对象被示例化之前修改类名称空间的机会,否则我们唯一的选择就是使用元类。
为解决这个问题而编写的任何元类至少必须:

  • 从名称空间中删除冲突的类属性/成员
  • 示例化类对象以创建插槽描述符
  • 保存对插槽描述符的引用
  • 将先前删除的成员及其值放回类__dict__中(以便dataclass机制可以找到它们)
  • 将class对象传递给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

真管用!

当前选项:一个setmember装饰器

通过一些努力,可以使用所谓的setmember装饰器以上述方式自动修改类,这需要偏离dataclasses 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语言改变slots机制以允许更大的灵活性,一种方法是改变slots描述符本身以在类定义时存储类级数据。
这可以通过提供一个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,而在其他插槽上不存在slot_member.value。这可以通过从新的slottools库导入空插槽工厂来适应:

from slottools import nullslot

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

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

上面建议的代码风格可能与dataclasses API不同,但是,slots机制本身甚至可以修改以支持这种风格的代码,特别要考虑到dataclasses 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,那么与槽冲突的默认字段值也可能是由数据类机制创建的。
nnsrf1az

nnsrf1az2#

在Python 3.10+中,你可以将slots=Truedataclass一起使用,以使其更高效地使用内存:

from dataclasses import dataclass

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

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

v8wbuo2f

v8wbuo2f3#

正如答案中所提到的,数据类中的数据类不能生成槽,原因很简单,槽必须在创建类之前定义。
事实上,针对数据类的PEP明确提到了这一点:
至少在最初的版本中,__slots__是不受支持的。__slots__需要在类创建时添加。数据类装饰器是在类创建后调用的,所以为了添加__slots__,装饰器必须创建一个新的类,设置__slots__,并返回它。因为这个行为有点令人惊讶,数据类的初始版本将不支持自动设置__slots__
我想使用slots是因为我需要初始化另一个项目中的很多很多数据类示例。我最终编写了自己的数据类替代实现,它支持这一点,还有一些额外的特性:dataclassy .
dataclassy使用了一种元类方法,这种方法有很多优点--它支持装饰器继承,大大降低了代码复杂度,当然还有插槽的生成。

from dataclassy import dataclass

@dataclass(slots=True)
class Pet:
    name: str
    age: int
    species: str
    fluffy: bool = True

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

23c0lvtd

23c0lvtd4#

我找到的解决这个问题的最简单的方法是使用object.__setattr__指定一个定制的__init__来赋值。

@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)
y1aodyip

y1aodyip5#

另一个解决方案是在类体内部从类型化注解生成slots参数,如下所示:

@dataclass
class Client:
    first: str
    last: str
    age_of_signup: int
    
     __slots__ = slots(__annotations__)

其中,slots函数为:

def slots(anotes: Dict[str, object]) -> FrozenSet[str]:
    return frozenset(anotes.keys())

运行该命令将生成一个slots参数,如下所示:frozenset({'first', 'last', 'age_of_signup})
这会将注解放在它上面,并生成一组指定的名称。这里的限制是必须为每个类重新键入__slots__ = slots(__annotations__)行,并且它必须位于所有注解的下面,并且它不适用于带有默认参数的注解。这样做的另一个优点是,slots参数永远不会与指定的注解冲突,因此您可以随意添加或删除成员,而不是担心维护sperate列表。

pcrecxhr

pcrecxhr6#

Rick Teacheysuggestion之后,我创建了一个slotted_dataclass装饰器。它可以在关键字参数中接受在没有__slots__的数据类中在[field]: [type] =之后指定的任何内容-字段和field(...)的默认值。指定应该进入旧的@dataclass构造函数的参数也是可能的,而是在dictionary对象中作为第一个位置参数。

@dataclass(frozen=True)
class Test:
    a: dict = field(repr=False)
    b: int = 42
    c: list = field(default_factory=list)

将变为:

@slotted_dataclass({'frozen': True}, a=field(repr=False), b=42, c=field(default_factory=list))
class Test:
    __slots__ = ('a', 'b', 'c')
    a: dict
    b: int
    c: list

下面是这个新装饰器的源代码:

def slotted_dataclass(dataclass_arguments=None, **kwargs):
    if dataclass_arguments is None:
        dataclass_arguments = {}

    def decorator(cls):
        old_attrs = {}

        for key, value in kwargs.items():
            old_attrs[key] = getattr(cls, key)
            setattr(cls, key, value)

        cls = dataclass(cls, **dataclass_arguments)
        for key, value in old_attrs.items():
            setattr(cls, key, value)
        return cls

    return decorator

代码说明

上面的代码利用了dataclasses模块通过在类上调用getattr来获取默认字段值的事实,这使得通过替换类的__dict__中的适当字段来提供默认值成为可能(在代码中使用setattr函数完成)。由@dataclass装饰器生成的类将与通过指定=之后的那些装饰器生成的类完全相同,就像我们在类不包含__slots__时所做的那样。
但是,由于具有__slots__的类的__dict__包含member_descriptor对象:

>>> class C:
...     __slots__ = ('a', 'b', 'c')
...
>>> C.__dict__['a']
<member 'a' of 'C' objects>
>>> type(C.__dict__['a'])
<class 'member_descriptor'>

一件很好的事情是备份这些对象,并在@dataclass装饰器完成工作后恢复它们,这是在代码中使用old_attrs字典完成的。

相关问题