如何使数据类更好地与 __slots__ 一起工作?
How can dataclasses be made to work better with __slots__?
它 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
字段协同工作?
2021 年更新:对 __slots__
的直接支持是 。我将这个答案留给后代,不会更新它。
该问题并非数据classes 独有。任何冲突的 class 属性都将在一个插槽中被踩踏:
>>> 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 级别的描述符对象:
>>> class Success:
... __slots__ = tuple("xyz")
...
>>>
>>> type(Success.x)
<class 'member_descriptor'>
为了防止这种冲突的变量名错误,class 命名空间必须在 之前更改 class 对象被实例化,这样就没有两个对象在 class:
中竞争相同的成员名称
- 指定的(默认)值*
- 插槽描述符(由插槽机制创建)
出于这个原因,父 class 上的 __init_subclass__
方法是不够的,class 装饰器也不够,因为在这两种情况下 class 对象在这些函数收到 class 更改它时已经创建。
当前选项:写元class
直到插槽机制被改变以允许更大的灵活性,或者语言本身提供了在实例化 class 对象之前改变 class 命名空间的机会,我们唯一的选择是使用元class.
为解决此问题而编写的任何元class 必须至少:
- 从命名空间
中删除冲突的classattributes/members
- 实例化 class 对象以创建插槽描述符
- 保存对插槽描述符的引用
- 将先前删除的成员及其值放回 class
__dict__
(以便 dataclass
机器可以找到它们)
- 将 class 对象传递给
dataclass
装饰器
- 将插槽描述符恢复到各自的位置
- 还考虑了很多极端情况(例如如果有
__dict__
插槽怎么办)
至少可以说,这是一项极其复杂的工作。像下面这样定义 class 会更容易——没有默认值,这样根本不会发生冲突——然后再添加一个默认值。
当前选项:在 class 对象实例化后进行更改
未更改的数据class 将如下所示:
@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
装饰器以上述方式自动更改 class。这将需要偏离 dataclasses API 以便在 class 体内以外的位置定义默认值,可能类似于:
@setmember(x=field(default=1))
@dataclass
class C:
__slots__="x"
x: int
同样的事情也可以通过父 class:
上的 __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 语言改变插槽机制以提供更大的灵活性。这样做的一种方法可能是更改插槽描述符本身以在 class 定义时存储 class 级别数据。
这可以通过提供 dict
作为 __slots__
参数来实现(见下文)。 class 级数据(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")
上面建议的代码风格与数据classes API 有偏差。然而,老虎机本身甚至可以被改变以允许这种代码风格,特别是考虑到数据 classes 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")
未来的可能性:在 class 正文中“准备”class 命名空间
另一种可能性是altering/preparing(与元class的__prepare__
方法同义)class命名空间。
目前,没有机会(除了写一个元class)在实例化class对象之前编写改变class命名空间的代码,并且插槽机制去上班。这可以通过创建一个用于预先准备 class 命名空间的钩子来改变,并使抱怨名称冲突的错误仅在该钩子被 运行.
之后产生
这个所谓的 __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__
method——它接收 class 命名空间并在创建 class 之前更改它.特别是对于这种情况,默认数据class 字段值将存储在其他一些方便的地方,以便在创建槽描述符对象后可以检索它们。
* 请注意,如果使用 dataclasses.field
,则与插槽冲突的默认字段值也可能由 dataclass 机器创建。
在 Rick Teachey's 之后,我创建了一个 slotted_dataclass
装饰器。在关键字参数中,它可以采用您在不带 __slots__
的数据 class 中 [field]: [type] =
之后指定的任何内容——字段的默认值和 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
模块通过调用 class 上的 getattr
获取默认字段值这一事实。这使得我们可以通过替换 class 的 __dict__
中的适当字段来提供我们的默认值(这是通过使用 setattr
函数在代码中完成的)。 @dataclass
装饰器生成的 class 将与通过在 =
之后指定那些生成的 class 完全相同,就像如果 class 没有包含 __slots__
.
但是由于 class 和 __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
字典在代码中完成的。
对于这个问题,我找到的最少涉及的解决方案是使用 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)
正如答案中已经指出的那样,来自数据classes 的数据classes 无法生成插槽,原因很简单,即必须在创建class 之前定义插槽。
事实上,PEP for data classes明确提到了这一点:
At least for the initial release, __slots__
will not be supported. __slots__
needs to be added at class creation time. The Data Class decorator is called after the class is created, so in order to add __slots__
the decorator would have to create a new class, set __slots__
, and return it. Because this behavior is somewhat surprising, the initial version of Data Classes will not support automatically setting __slots__
.
我想使用插槽,因为我需要在另一个项目中初始化很多很多数据 class 实例。我最终编写了自己的数据 classes 替代实现,它支持这一点,其中包括一些额外的功能:dataclassy.
dataclassy 使用具有许多优点的 metaclass 方法 - 它支持装饰器继承,显着降低代码复杂性,当然还有槽的生成。使用 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__
已成功生成并且有效。另外,正如所证明的那样,默认值工作得很好。
另一种解决方案是在 class 主体内从类型化注释生成插槽参数。
这看起来像:
@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})
这会采用上面的注释并生成一组指定的名称。这里的限制是您必须 re-type 每个 class 的 __slots__ = slots(__annotations__)
行,并且它必须位于所有注释的下方,并且它不适用于具有默认参数的注释。
这还有一个好处,即 slots 参数永远不会与指定的注释冲突,因此您可以随意添加或删除成员,而不必担心维护单独的列表。
在 Python 3.10+ 中,您可以将 slots=True
与 dataclass
一起使用以提高内存效率:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Point:
x: int = 0
y: int = 0
这样您也可以设置默认字段值。
它 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
字段协同工作?
2021 年更新:对 __slots__
的直接支持是
该问题并非数据classes 独有。任何冲突的 class 属性都将在一个插槽中被踩踏:
>>> 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 级别的描述符对象:
>>> class Success:
... __slots__ = tuple("xyz")
...
>>>
>>> type(Success.x)
<class 'member_descriptor'>
为了防止这种冲突的变量名错误,class 命名空间必须在 之前更改 class 对象被实例化,这样就没有两个对象在 class:
中竞争相同的成员名称- 指定的(默认)值*
- 插槽描述符(由插槽机制创建)
出于这个原因,父 class 上的 __init_subclass__
方法是不够的,class 装饰器也不够,因为在这两种情况下 class 对象在这些函数收到 class 更改它时已经创建。
当前选项:写元class
直到插槽机制被改变以允许更大的灵活性,或者语言本身提供了在实例化 class 对象之前改变 class 命名空间的机会,我们唯一的选择是使用元class.
为解决此问题而编写的任何元class 必须至少:
- 从命名空间 中删除冲突的classattributes/members
- 实例化 class 对象以创建插槽描述符
- 保存对插槽描述符的引用
- 将先前删除的成员及其值放回 class
__dict__
(以便dataclass
机器可以找到它们) - 将 class 对象传递给
dataclass
装饰器 - 将插槽描述符恢复到各自的位置
- 还考虑了很多极端情况(例如如果有
__dict__
插槽怎么办)
至少可以说,这是一项极其复杂的工作。像下面这样定义 class 会更容易——没有默认值,这样根本不会发生冲突——然后再添加一个默认值。
当前选项:在 class 对象实例化后进行更改
未更改的数据class 将如下所示:
@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
装饰器以上述方式自动更改 class。这将需要偏离 dataclasses API 以便在 class 体内以外的位置定义默认值,可能类似于:
@setmember(x=field(default=1))
@dataclass
class C:
__slots__="x"
x: int
同样的事情也可以通过父 class:
上的__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 语言改变插槽机制以提供更大的灵活性。这样做的一种方法可能是更改插槽描述符本身以在 class 定义时存储 class 级别数据。
这可以通过提供 dict
作为 __slots__
参数来实现(见下文)。 class 级数据(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")
上面建议的代码风格与数据classes API 有偏差。然而,老虎机本身甚至可以被改变以允许这种代码风格,特别是考虑到数据 classes 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")
未来的可能性:在 class 正文中“准备”class 命名空间
另一种可能性是altering/preparing(与元class的__prepare__
方法同义)class命名空间。
目前,没有机会(除了写一个元class)在实例化class对象之前编写改变class命名空间的代码,并且插槽机制去上班。这可以通过创建一个用于预先准备 class 命名空间的钩子来改变,并使抱怨名称冲突的错误仅在该钩子被 运行.
之后产生这个所谓的 __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__
method——它接收 class 命名空间并在创建 class 之前更改它.特别是对于这种情况,默认数据class 字段值将存储在其他一些方便的地方,以便在创建槽描述符对象后可以检索它们。
* 请注意,如果使用 dataclasses.field
,则与插槽冲突的默认字段值也可能由 dataclass 机器创建。
在 Rick Teachey's slotted_dataclass
装饰器。在关键字参数中,它可以采用您在不带 __slots__
的数据 class 中 [field]: [type] =
之后指定的任何内容——字段的默认值和 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
模块通过调用 class 上的 getattr
获取默认字段值这一事实。这使得我们可以通过替换 class 的 __dict__
中的适当字段来提供我们的默认值(这是通过使用 setattr
函数在代码中完成的)。 @dataclass
装饰器生成的 class 将与通过在 =
之后指定那些生成的 class 完全相同,就像如果 class 没有包含 __slots__
.
但是由于 class 和 __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
字典在代码中完成的。
对于这个问题,我找到的最少涉及的解决方案是使用 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)
正如答案中已经指出的那样,来自数据classes 的数据classes 无法生成插槽,原因很简单,即必须在创建class 之前定义插槽。
事实上,PEP for data classes明确提到了这一点:
At least for the initial release,
__slots__
will not be supported.__slots__
needs to be added at class creation time. The Data Class decorator is called after the class is created, so in order to add__slots__
the decorator would have to create a new class, set__slots__
, and return it. Because this behavior is somewhat surprising, the initial version of Data Classes will not support automatically setting__slots__
.
我想使用插槽,因为我需要在另一个项目中初始化很多很多数据 class 实例。我最终编写了自己的数据 classes 替代实现,它支持这一点,其中包括一些额外的功能:dataclassy.
dataclassy 使用具有许多优点的 metaclass 方法 - 它支持装饰器继承,显着降低代码复杂性,当然还有槽的生成。使用 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__
已成功生成并且有效。另外,正如所证明的那样,默认值工作得很好。
另一种解决方案是在 class 主体内从类型化注释生成插槽参数。 这看起来像:
@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})
这会采用上面的注释并生成一组指定的名称。这里的限制是您必须 re-type 每个 class 的 __slots__ = slots(__annotations__)
行,并且它必须位于所有注释的下方,并且它不适用于具有默认参数的注释。
这还有一个好处,即 slots 参数永远不会与指定的注释冲突,因此您可以随意添加或删除成员,而不必担心维护单独的列表。
在 Python 3.10+ 中,您可以将 slots=True
与 dataclass
一起使用以提高内存效率:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Point:
x: int = 0
y: int = 0
这样您也可以设置默认字段值。