Проблема не уникальна для классов данных. ЛЮБОЙ конфликтующий атрибут класса растопчет слот:
class Failure:
__slots__ = tuple("xyz")
x=1
# ERROR
Это просто, как работают слоты. Чтобы предотвратить это, пространство имен класса должно быть изменено за до создание экземпляра объекта класса таким образом, чтобы не было двух конкурирующих объектов, конкурирующих за один и тот же слот в членах объекта класса:
- указанное (по умолчанию) значение (или объект поля)
- дескриптор члена, созданный механизмом slots
По этой причине метод __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
Другим потенциальным способом решения проблемы может быть добавление служебной функции dataclass_slots
в API классов данных (или в отдельный отдельный API с собственным декоратором).
Может работать что-то вроде следующего:
@slotted_dataclass
class C:
__slots__ = dataclass_slots(x=field(default=1))
x: int
Объект, возвращаемый функцией dataclass_slots
, будет итеративным и позволит работать существующему механизму слотов. Однако это также позволило бы декоратору slotted_dataclass
соответствующим образом создавать объекты полей, методы и т. Д. Впоследствии.