
Всем привет! Меня зовут Дима. Я являюсь Backend Python Developer'ом. Сегодня расскажу Вам про «волшебный» инструмент __slots__
в Python.
Оглавление
Что такое слоты?
Когда лучше использовать?
Когда лучше НЕ использовать?
Итог
Что такое слоты?
Слоты или __slots__
- это атрибут, параметр или Dunder-метод, позволяющий оптимально потреблять память, выделенную под объект. Также не позволяет добавлять атрибуты экземпляра, не прописанные в слотах.
Также этот Dunder-метод позволяет ограничить создание новых атрибутов в экземплярах класса. Он может определяться как кортеж или список имён атрибутов (пример, __slots__ = ('attr_name_1', 'attr_name_2', ...)
) в определении класса. Если в классе необходим только один атрибут, и при этом нужно использовать __slots__
, то атрибут можно указать в качестве строки (пример, __slots__ = 'attr_name'
).
В таком случае экземпляры класса могут иметь только атрибуты с именами, определёнными в __slots__
. Попытка создать новый атрибут приведёт к ошибке.
Обратите внимание
Использование слотов или __slots__
экономит память, так как экземпляры не создают словарь __dict__
для хранения атрибутов (по умолчанию класс создаёт __dict__
). Слоты не наследуются, поэтому для каждого подкласса необходимо определять свои слоты
Лучшие примеры использования (см. ниже).
Когда лучше использовать?
1) Когда нет необходимости динамически расширять или дополнять объект (класс) новыми полями (атрибутами). Известно количество полей в классе заранее (в основном это самый частый пример на моей практике).
2) Когда много экземпляров объекта (класса), занимаемых большой кусок памяти. Например, в память необходимо выгрузить большой объём данных, состоящих из точек 2D координат в виде классов Python.
Необходимые импорты для тестирования:
from pympler import asizeof
Пример использования обычного класса:
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __str__(self) -> str:
return f'Point(x={self.x}, y={self.y})'
point = Point(1.2, 3.4)
print(point)
print(f'Экземпляр класса `point` занимает {asizeof.asizeof(point)} байт.')
Point(x=1.2, y=3.4)
Экземпляр класса `point` занимает 488 байт.
Пример использования обычного класса вместе со __slots__
:
class Point:
__slots__ = ('x', 'y')
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __str__(self) -> str:
return f'Point(x={self.x}, y={self.y})'
point = Point(1.2, 3.4)
print(point)
print(f'Экземпляр класса `point` занимает {asizeof.asizeof(point)} байт.')
Point(x=1.2, y=3.4)
Экземпляр класса `point` занимает 96 байт.
3) Быстрее доступ к полям экземпляра, так как используется кортеж для хранения полей.
Необходимые импорты для тестирования:
from timeit import timeit
Пример использования обычного класса:
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def test_call_attr_from_point() -> None:
point = Point(1.2, 3.4)
point.x
number = 100_000
print(f'Кол-во выполнений: {number} раз. Время выполнения кода {timeit(test_call_attr_from_point, number=number)} сек.')
print(f'Кол-во выполнений: {number} раз. Время выполнения кода {timeit(test_call_attr_from_point, number=number)} сек.')
print(f'Кол-во выполнений: {number} раз. Время выполнения кода {timeit(test_call_attr_from_point, number=number)} сек.')
Кол-во выполнений: 100000 раз. Время выполнения кода 0.013771874997473788 сек.
Кол-во выполнений: 100000 раз. Время выполнения кода 0.01267316599842161 сек.
Кол-во выполнений: 100000 раз. Время выполнения кода 0.012301832997763995 сек.
Пример использования обычного класса вместе со __slots__
:
class Point:
__slots__ = ('x', 'y')
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def test_call_attr_from_point() -> None:
point = Point(1.2, 3.4)
point.x
number = 100_000
print(f'Кол-во выполнений: {number} раз. Время выполнения кода {timeit(test_call_attr_from_point, number=number)} сек.')
print(f'Кол-во выполнений: {number} раз. Время выполнения кода {timeit(test_call_attr_from_point, number=number)} сек.')
print(f'Кол-во выполнений: {number} раз. Время выполнения кода {timeit(test_call_attr_from_point, number=number)} сек.')
Кол-во выполнений: 100000 раз. Время выполнения кода 0.013620709003589582 сек.
Кол-во выполнений: 100000 раз. Время выполнения кода 0.011800416999903973 сек.
Кол-во выполнений: 100000 раз. Время выполнения кода 0.011018457997124642 сек.
Не смотря на лучшие практики использования, есть случаи, когда их лучше не применять (см. ниже).
Когда лучше НЕ использовать?
1) Когда возникает необходимости динамически расширять класс атрибутами (новыми полями).
2) Когда есть логика, завязанная на instance.__dict__
(по умолчанию поля класса хранятся в словаре).
На этой ноте можно подвести итог (см. ниже).
Итог
Советую использовать вышеуказанный инструмент для оптимизации Вашего Python кода как по скорости выполнения (в некоторых моментах), так и по памяти. Также советую всё локально тестировать и проверять на около реальных (рабочих) примерах.
Слоты также можно использовать вместе с модулем from dataclasses import dataclass
. Но это немного другая история :-)
Комментарии (15)
Andrey_Solomatin
19.02.2025 19:02Никогда не использовать.
Попытка оптимизировать Питон через слоты показывает большой просчет в архитектуре.
На маленьком количестве данных разницы не будет заметно, а когда данных много стоит смотреть в сторону Restricted Computation Domain. Например numpy, который хранит данные с минимальным оверхедом.CrazyOpossum
19.02.2025 19:02Использую слоты, но не ради перфоманса, а как самодокументацию и защиту от соблазнов что-нибудь добавить вне init.
Andrey_Solomatin
19.02.2025 19:02Я иммутабельный NamedTuple или pydantic.BaseModel если нужно с сериализацией работать. Я верь своим коллегам и не делаю дополнительной работы по защите.
CrazyOpossum
19.02.2025 19:02В принципе, ничего против не имею, но контейнерам контейнерово, а в общем случае - slots. Если я вижу NamedTuple, dataclass, attrs или BaseModel, то я не ожидаю какой-то серьёзной логики в классе.
Andrey_Solomatin
19.02.2025 19:02А что слоты дают для сомодокументации?
danilovmy
19.02.2025 19:02Для содомодокументации многое даст политика документации принятая в проекте. А слоты стоят часто на первом месте после объявления класса и вместе c корректным названием класса могут заменить docstring, так еще и бенефит по памяти, который никаким педантиком и датаклассами не достичь.
Andrey_Solomatin
19.02.2025 19:02так еще и бенефит по памяти, который никаким педантиком и датаклассами не достичь.
И скорее всего не померить :)
class Point(NamedTuple): x: int y: int
Вроде тоже достаточно самодокументирующийся код.
danilovmy
19.02.2025 19:02Ну почему же не померить.
However, what is most surprising is that namedtuple instances take a bit more memory than instances with slots.Или тут: https://tommyseattle.com/python-class-dict-named-tuple-performance-and-memory-usage/
Andrey_Solomatin
19.02.2025 19:02Я про дашборд в графане про потребление памяти и загрузку процессора вашим приложением.
Вы создаёте 264 тысячи инстансов в секунду? если нет, то классы справятся с такой нагрузкой.
В один мегабайт умещается 1500 классов и 5500 классов со слотами.
У меня приложение которое держит в памяти около десяти тысяч pydantic объектов потребляет 200-400MB. Ну упорюсь я, его на слоты перепишу, сколько я выиграю? Кроме самих данных, там куча словарей для упрощения вычислений.
15 процентов процессора потребляет в течении минуты раз в 15 минут. От шумных соседей будет больше разницы чем от слотов.
Kreastr
А не маловато ли итераций в бенчмарке? 0.013 секунд на все выглядит как значение очень чувствительное к джиттеру от переключенич задач в ОС.