В классе объявили slots без dict : где Python ожидает хранить атрибуты экземпляра?

В классе объявили __slots__ без __dict__: где Python ожидает хранить атрибуты экземпляра?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

При наличии __slots__ без __dict__ атрибуты экземпляра хранятся не в обычном словаре экземпляра, а в слотах, описанных классом. Произвольное добавление атрибута, отсутствующего среди этих слотов, завершается ошибкой AttributeError.

Исторический контекст

Обычный экземпляр Python-класса обычно хранит атрибуты в __dict__. Это удобно: объект можно расширять во время выполнения, но словарь требует дополнительной памяти и допускает случайные имена атрибутов.

__slots__ появился как механизм явного описания набора экземплярных атрибутов. Он позволяет отказаться от словаря экземпляра, уменьшить накладные расходы для большого числа однотипных объектов и раньше обнаруживать ошибки в именах атрибутов.

Постановка проблемы

Если класс создаёт миллионы небольших объектов с фиксированным набором данных, отдельный словарь у каждого экземпляра может быть неоправданно дорогим. С другой стороны, код или библиотека могут рассчитывать на динамическое добавление атрибутов, сериализацию через __dict__ или использование слабых ссылок.

Неверный выбор приводит либо к лишнему потреблению памяти, либо к ошибкам выполнения в местах, где ранее можно было присвоить новый атрибут. Поэтому __slots__ — это не просто оптимизация, а изменение модели экземпляра.

Подробное решение

Каждое имя в __slots__ создаёт в классе специальный дескриптор хранения. При чтении и записи такого атрибута Python обращается к этому дескриптору, а значение размещается в слоте экземпляра.

Если __slots__ не содержит __dict__, у экземпляра нет обычного словаря атрибутов. Поэтому присваивание имени, которого нет среди слотов, невозможно:

class Point: __slots__ = ('x', 'y') p = Point() p.x = 1 p.y = 2 try: p.color = 'red' except AttributeError: print('динамический атрибут запрещён')

Здесь x и y разрешены, а color — нет. Это ограничение действует на конкретный экземпляр, но не означает, что класс полностью неизменяем: атрибуты класса по-прежнему можно изменять, если это допускает код.

Если добавить __dict__ в __slots__, экземпляр снова сможет хранить произвольные атрибуты, однако преимущество отказа от словаря теряется. Аналогично, для поддержки слабых ссылок обычно требуется явно предусмотреть слот __weakref__.

При наследовании нужно анализировать всю иерархию. Подкласс, в котором __slots__ не объявлен, обычно получает собственный __dict__, даже если базовый класс использует слоты. Поэтому контроль набора атрибутов может быть нарушен в подклассе.

__slots__ не делает объект неизменяемым и не заменяет валидацию значений. Он ограничивает главным образом набор мест хранения атрибутов; значения слотов можно переassignивать, если дополнительно не установлен дескриптор или свойство с другими правилами.

Ситуация из практики

В сервисе создаются миллионы объектов координат, у каждого строго фиксированы три числовых поля. Вариант с обычным классом прост и совместим с динамическими атрибутами, но каждый экземпляр несёт накладные расходы словаря. Вариант с __slots__ уменьшает модель хранения и предотвращает опечатки в именах полей, но требует проверить сериализацию, наследование и инструменты отладки.

Добавление __dict__ сохраняет гибкость, но снижает смысл оптимизации. Выбран класс со слотами без __dict__, потому что схема объекта стабильна, а все места, где требуются дополнительные данные, были вынесены во внешний индекс.

В результате случайные атрибуты стали обнаруживаться сразу при тестировании, а потребление памяти уменьшилось. Перед таким изменением необходимо измерять эффект на целевом Python и конкретном размере объектов, а не считать экономию гарантированной для любого класса.

Что кандидаты часто упускают

  1. Обязательно ли наличие __slots__ в базовом классе, чтобы подкласс не имел __dict__?

Нет. Если подкласс не объявляет собственный __slots__, он обычно получает словарь экземпляра. Чтобы сохранить ограничение, в каждом соответствующем классе иерархии явно объявляют __slots__, часто с пустым набором в подклассах, которым не нужны новые поля.

Это важно при проектировании библиотечного базового класса: один свободный подкласс может вернуть динамические атрибуты всей последующей ветке наследования.

  1. Можно ли использовать __slots__ для запрета изменения уже существующих атрибутов?

Нет. Слот ограничивает наличие и способ хранения атрибута, но обычная запись в него остаётся разрешённой. Если нужно запретить изменение или проверять значение, применяют свойства, дескрипторы, переопределение __setattr__ либо другую явную логику.

Следовательно, __slots__ решает проблему структуры экземпляра, а не проблему неизменяемости или валидации данных.

  1. Почему сериализация через __dict__ может перестать работать после перехода на __slots__?

У экземпляра без слота __dict__ нет атрибута __dict__, поэтому код, который собирает состояние через obj.__dict__, завершится ошибкой или не увидит слотированные поля. Сериализатор должен получать данные через заранее известные имена, специальные методы сериализации или поддержку слотов.

Это один из главных практических компромиссов: более компактное хранение связывает внутреннее представление объекта с контрактом инструментов, работающих с его состоянием.