Как сделать так, чтобы классы данных работали лучше с __slots__? - PullRequest
0 голосов
/ 04 мая 2018

В было решено убрать прямую поддержку __slots__ из классов данных для Python 3.7.

Несмотря на это, __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 по умолчанию для совместной работы?

1 Ответ

0 голосов
/ 04 мая 2018

Проблема не уникальна для классов данных. ЛЮБОЙ конфликтующий атрибут класса растопчет слот:

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 соответствующим образом создавать объекты полей, методы и т. Д. Впоследствии.

...