Когда прям с заголовка набросил, то дальше уже всегда проще.
Давайте посмотрим, какую пользу и какой вред приносит использование
@dataclass(slots=True) или @attr.define(slots=True). В целом - различий не так много.Во-первых, что делает
__slots__ = ('a',) внутри класса?
class My:
__slots__ = ('a',)
1. Валидирует, что
__slots__ корректны2. Генерирует дескрипторы для всех имен в
__slots__, см https://github.com/python/cpython/blob/91ff700de28f3415cbe44f58ce84a2670b8c9f15/Objects/descrobject.c#L793-L796
>>> class My:
... __slots__ = ('a',)
...
>>> My.a, type(My.a)
(<member 'a' of 'My' objects>, <class 'member_descriptor'>)
3. Если слот
__dict__ не проставлен, то меняет базовую функцию доступа к и установки аттрибутов
/* Special case some slots */
if (type->tp_dictoffset != 0 || ctx->nslot > 0) {
PyTypeObject *base = ctx->base;
if (base->tp_getattr == NULL && base->tp_getattro == NULL) {
type->tp_getattro = PyObject_GenericGetAttr;
}
if (base->tp_setattr == NULL && base->tp_setattro == NULL) {
type->tp_setattro = PyObject_GenericSetAttr;
}
}
Из-за чего больше нельзя будет назначать произвольные атрибуты:
>>> class My:
... __slots__ = ('a',)
...
>>> m = My()
>>> m.custom = 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'My' object has no attribute 'custom'
Но, достаточно добавить
'__dict__' внутрь __slots__, чтобы вернуть данное поведение: __slots__ = ('a', '__dict__'):
>>> class My:
... __slots__ = ('a', '__dict__')
...
>>> m = My()
>>> m.custom = 0
4.
__slots__ ускоряет доступ к атрибутам и уменьшает размер объектов.
>>> import sys
>>> class A:
... __slots__ = ('a',)
... def __init__(self, a):
... self.a = a
>>> class B:
... def __init__(self, a):
... self.a = a
>>> sys.getsizeof(A(1))
40
>>> sys.getsizeof(B(1))
56
Разница в скорости доступа не очень большая, но есть:
» pyperf timeit -s '
class A:
def __init__(self, a):
self.a = a
a = A(1)' 'a.a'
.....................
Mean +- std dev: 13.9 ns +- 0.1 ns
Против доступа со
__slots__:
» pyperf timeit -s '
. class A:
. __slots__ = ("a",)
. def __init__(self, a):
. self.a = a
.
. a = A(1)' 'a.a'
.....................
Mean +- std dev: 13.3 ns +- 0.1 ns
Так что там с датаклассами?
Штука в том, что создать слоты в существующем классе – нельзя физически. Слишком много всего написано на C.
Можно только пересоздать класс еще раз 😱
https://github.com/python/cpython/blob/91ff700de28f3415cbe44f58ce84a2670b8c9f15/Lib/dataclasses.py#L1224-L1276
cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
Что порождает много проблем. Например:
- Нельзя использовать
super() без параметров в методах внутри тела класса:
>>> @dataclass(slots=True)
... class My:
... def __str__(self) -> str:
... return super().__str__()
...
>>> str(My())
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
str(My())
~~~^^^^^^
File "<python-input-1>", line 4, in __str__
return super().__str__()
^^^^^^^^^^^^^^^
TypeError: super(type, obj): obj (instance of My) is not an instance or subtype of type (My).
Потому что
__str__.closure не обновляет cell объекты на другой класс при пересоздании. Есть PR, но все сложно: https://github.com/python/cpython/pull/111538- Нельзя использовать `__init_subclass__` в
родителях класса со slots, где ожидаются параметры. Тут только документацией можно помочь: https://github.com/python/cpython/pull/123342Так что - будьте осторожны!