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

时间:2018-05-04 18:02:24

标签: python python-3.x slots python-dataclasses

was decided从Python 3.7的数据类中删除对.Show的直接支持。

尽管如此,__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__字段如何协同工作?

5 个答案:

答案 0 :(得分:4)

正如答案中已经提到的那样,出于简单的原因,必须在创建类之前定义插槽,因此数据类中的数据类无法生成插槽。

实际上,PEP for data classes明确提到了这一点:

至少对于初始发行版,将不支持__slots____slots__需要在类创建时添加。创建类后,将调用数据类装饰器,因此,要添加__slots__,装饰器必须创建一个新类,设置__slots__,然后返回它。由于这种行为有些令人惊讶,因此数据类的初始版本将不支持自动设置__slots__

我想使用插槽,因为我需要在另一个项目中初始化许多数据类实例。我最终编写了自己的数据类替代实现,其中包括一些附加功能,这些实现支持此功能:dataclassy

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__已成功生成并有效。另外,可以证明,默认值可以正常工作。

答案 1 :(得分:3)

问题不是数据类所特有的。任何冲突的类属性都会占用整个插槽:

class Failure:
    __slots__ = tuple("xyz")
    x=1
# ERROR

这就是插槽的工作原理。为了防止这种情况,必须在实例化类对象之前更改类名称空间,以便在类对象成员中没有两个竞争对象竞争相同的插槽:

  • 指定的(默认)值(或字段对象)
  • 由slot machinery创建的成员描述符

因此,父类的__init_subclass__方法是不够的,类装饰器也不够,因为在这两种情况下都已经创建了类对象。

直到改变插槽机械以提供更大的灵活性,我们唯一的选择是使用元类。

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

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

至少可以说,这是一项非常复杂的工作。如下所示定义类会更容易 - 因此冲突根本不会发生 - 然后在之后更改它,以便数据类字段具有所需的默认值。

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

改变是直截了当的。更改__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

有效!

通过一些努力,可以使用所谓的slotted_dataclass装饰器以上述方式自动改变类。这将需要偏离数据类API - 可能类似于:

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

同样的事情也可以通过父类的__init_subclass__方法来实现:

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

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

解决问题的另一种可能方法可能是向数据类API添加dataclass_slots实用程序函数(或者使用自己的装饰器添加到自定义的单独API)。

以下内容可能有效:

@slotted_dataclass
class C:
    __slots__ = dataclass_slots(x=field(default=1))
    x: int

dataclass_slots函数返回的对象是可迭代的,允许现有的slot机器工作。但是它也允许slotted_dataclass装饰器在之后适当地创建场对象,方法等。

答案 2 :(得分:1)

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

@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__一样。

但是由于具有__dict__的类的__slots__包含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字典在代码中完成的。

答案 3 :(得分:1)

针对该问题,我发现涉及最少的解决方案是使用__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)

答案 4 :(得分:0)

另一种解决方案是根据键入的注释在类主体内部生成slot参数。 看起来可能像这样:

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

运行将生成一个广告位参数,该参数如下所示: frozenset({'first', 'last', 'age_of_signup})

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