Экономия памяти со __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 ... теперь слоты используются корректно в подклассе