
Всем привет! Меня зовут Дима. Я являюсь 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
. Но это немного другая история :-)