Короткий ответ
__slots__ может заметно снизить потребление памяти и немного ускорить доступ к атрибутам при массовом создании однотипных мелких объектов, но ограничивает динамичность класса (нельзя произвольно добавлять атрибуты, сложнее совмещается с множественным наследованием и некоторыми библиотеками, ожидающими __dict__), поэтому повсеместное применение без разбора рискует сломать совместимость с существующим кодом и сторонними библиотеками, которые полагаются на динамические атрибуты, monkey-patching или сериализацию через __dict__.
Как это работает подробнее
Экономия памяти от __slots__ пропорциональна количеству экземпляров и разнице между размером __dict__ экземпляра (который выделяется для каждого объекта индивидуально и имеет заметный базовый оверхед) и компактным slot-хранилищем на уровне дескрипторов класса; выигрыш особенно заметен именно на больших количествах мелких объектов, что соответствует описанному сценарию кэша. Однако любой код, который сериализует объект через obj.__dict__ (некоторые реализации pickle, кастомные сериализаторы, debug-инструменты), или который ожидает возможности динамически добавить атрибут объекту (некоторые паттерны мокирования в тестах, декораторы, добавляющие служебные поля), перестанет работать без изменений, если у объекта нет __dict__.
Пример кода
import sys
class CacheEntrySlots:
__slots__ = ("key", "value", "expires_at")
def __init__(self, key, value, expires_at):
self.key, self.value, self.expires_at = key, value, expires_at
class CacheEntryDict:
def __init__(self, key, value, expires_at):
self.key, self.value, self.expires_at = key, value, expires_at
e1 = CacheEntrySlots("k", "v", 123)
e2 = CacheEntryDict("k", "v", 123)
print(sys.getsizeof(e1), sys.getsizeof(e2)) # slots-версия заметно компактнее
try:
e1.extra = "x" # нельзя добавить произвольный атрибут
except AttributeError as e:
print("Ошибка:", e)Что использовать на практике
- Практика. Внедрение стоит начинать с классов, которые действительно создаются в больших количествах и имеют стабильный, точно известный набор полей (записи кэша, узлы структур данных), измеряя реальный эффект на памяти через профилирование (
sys.getsizeof,tracemalloc) в условиях, близких к продакшн-нагрузке, а не на синтетическом микробенчмарке. Для классов с наследованием важно либо применять__slots__на всех уровнях иерархии согласованно, либо явно принять, что часть экономии потеряется там, где какой-то уровень иерархии не задаёт__slots__(и тогда у потомков появляется__dict__в любом случае).
Подводные камни
- Риски. Внедряют
__slots__массово по всей кодовой базе одним большим изменением без оценки, какие классы реально выигрывают по памяти, и получают неожиданныеAttributeErrorв местах, где код (возможно, в сторонней библиотеке или тестах) динамически добавлял атрибуты. Также не проверяют поведение сериализации/копирования таких объектов (pickle по умолчанию работает с__slots__, но требует, чтобы класс определил нужные методы корректно в более сложных случаях наследования), из-за чего ломаются существующие механизмы сохранения состояния. - Компромисс. Выигрыш в памяти и небольшой выигрыш в скорости доступа к атрибутам стоит потери гибкости класса и риска несовместимости с кодом/библиотеками, ожидающими
__dict__; решение должно быть точечным — применяться к конкретным классам с измеримым профитом на реальной нагрузке, а не как универсальная практика "для всех классов сразу", особенно в кодовой базе, где не весь код и не все зависимости находятся под полным контролем команды.