Экономия памяти со __slots__
В Python атрибуты классов по-умолчанию хранятся в специальном dunder-атрибуте __dict__. В описании класса его задавать не надо, он есть неявно и доступен для просмотра при необходимости. Каждый экземпляр класса также имеет свой __dict__:
class Standard:
def __init__(self, x, y):
self.x = x
self.y = y
std = Standard(100, 200)
std.__dict__ # {'x': 100, 'y': 200}
Помимо того, что и класс и экземпляры отдельно занимают своими __dict__ место в памяти, хранение данных в словарях само по себе несет большие накладные расходы. Хеш-таблица в основе словаря хранит служебные структуры и растёт скачками при увеличении числа атрибутов, поэтому на больших количествах объектов затраты памяти ощутимы:
from sys import getsizeof
std_size = getsizeof(std) + getsizeof(std.__dict__)
std_size # 344 байта
Один из эффективных способов сэкономить память, это реализовать в классе специальный атрибут __slots__ и объявить в нем последовательность атрибутов экземпляра. Тогда вместо __dict__, Python будет использовать альтернативную структуру хранения атрибутов с помощью дескрипторов. __slots__ для экземпляров классов отдельно не создается и хранится только на уровне класса:
class Slot:
__slots__ = ('x', 'y') # Неизменный кортеж из имен атрибутов
def __init__(self, x, y): # Остальное – без изменений
self.x = x
self.y = y
slt = Slot(100, 200)
slt.__dict__ # **AttributeError**: 'Slot' object has no attribute '__dict__'. Did you mean: '__dir__'?
slt_size = getsizeof(slt)
slt_size # 48 байтов
Так добавив одну строчку кода, можно сэкономить расходы памяти в приложении, где требуется создавать миллионы одинаковых объектов.
---
Важные ограничения
Стоит отметить, что реализация __slots__ запрещает динамически добавлять экземпляру класса атрибуты, в отличие от __dict__. В ситуациях, где такое необходимо, __slots__ не подойдет.
std.z = 300
std.__dict__ # {'x': 100, 'y': 200, 'z': 300}
slt.z = 300 # **AttributeError**: 'Slot' object has no attribute 'z' and no __dict__ for setting new attributes
Важно, не забывать расширять слоты, если мы добавляем в код класса новые атрибуты:
class PartialSlots:
__slots__ = ('x', 'y') # Не добавили атрибут экземпляра 'z'
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
p = PartialSlots(100, 200, 300) # **AttributeError**: 'PartialSlots' object has no attribute 'z' and no __dict__ for setting new attributes
В подклассах от класса со __slots__ наследование этого атрибута проходит лишь частично. Для полноценного использования, его стоит определить еще раз, включив новые атрибуты подкласса:
# Подкласс без доп. логики
class InheritSlot(Slot):
pass
inh_slt = InheritSlot(100, 200)
inh_slt.__dict__ # {}, атрибут снова доступен
inh_slt.z = 300 # Нет ошибок при динамическом расширении атрибутов
inh_slt.__dict__ # {'z': 300}, словарь подкласса снова занимает память
# Поправим
class InheritSlot(Slot):
__slots__ = ('z', ) # Слоты суперкласса добавятся в начало кортежа. В конце не забываем запятую, так как это кортеж из одного элемента.
inh_slt2 = InheritSlot(100, 200, 300)
inh_slt2.__dict__ # AttributeError ... теперь слоты используются корректно в подклассе