2021 年更新:直接支持__slots__
is 添加到Python 3.10 https://stackoverflow.com/a/69661861/758345。我将这个答案留给后代,并且不会更新它。
这个问题并不是数据类所独有的。任何冲突的类属性都会踩在槽上:
>>> 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'>
为了防止这种变量名冲突错误,必须更改类名称空间before类对象被实例化,以便不存在两个对象竞争类中的相同成员名称:
为此,一个__init_subclass__
父类上的方法是不够的,类装饰器也不够,因为在这两种情况下,当这些函数接收到要更改它的类时,类对象已经创建了。
当前选项:编写元类
直到槽机制被改变以允许更大的灵活性,或者语言本身提供了在类对象实例化之前改变类名称空间的机会,我们唯一的选择是使用元类。
为解决此问题而编写的任何元类至少必须:
- 从命名空间中删除冲突的类属性/成员
- 实例化类对象以创建槽描述符
- 保存对槽描述符的引用
- 将之前删除的成员及其值放回到类中
__dict__
(所以dataclass
机器可以找到它们)
- 将类对象传递给
dataclass
装饰者
- 将槽描述符恢复到各自的位置
- 还要考虑大量的极端情况(例如,如果存在
__dict__
slot)
至少可以说,这是一项极其复杂的工作。像下面这样定义类会更容易 - 没有默认值,这样根本就不会发生冲突 - 然后添加默认值。
当前选项:在类对象实例化后进行更改
未更改的数据类将如下所示:
@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
Test:
>>> 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
有用!
当前选项:asetmember
装饰者
经过一番努力,所谓的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
as the __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__
hook 可能看起来像这样,我认为这还不错:
from dataclasses import dataclass, prepare_slots
@dataclass
class C:
__slots__ = ('x',)
__prepare_slots__ = prepare_slots
x: int = field(default=1)
The dataclasses.prepare_slots
函数只是一个函数——类似于__prepare__ method https://docs.python.org/3/reference/datamodel.html#preparing-the-class-namespace-- 接收类名称空间并在创建类之前更改它。特别是对于这种情况,默认数据类字段值将存储在其他方便的位置,以便在创建槽描述符对象后可以检索它们。
* 请注意,与槽冲突的默认字段值也可能由数据类机制创建,如果dataclasses.field
正在使用中。