При создании тысяч однотипных объектов память неожиданно велика: как словари экземпляров могут разделять структуру ключей?
В CPython словари __dict__ однотипных экземпляров могут использовать общую таблицу ключей, а каждый объект хранит только собственные значения. Это называется key-sharing dictionaries или разделяемыми словарями ключей: повторяющиеся имена атрибутов не дублируются в каждом экземпляре.
Экономия возникает только при совместимом формате экземпляров. Динамическое добавление необычных атрибутов, различающаяся структура объектов и особенности реализации могут уменьшить или устранить этот эффект.
Механизм появился в CPython 3.3 в рамках оптимизации словарей, описанной в PEP 412. Исходная проблема состояла в том, что объектно-ориентированные программы создают много экземпляров одного класса, а обычный словарь каждого экземпляра отдельно хранил одинаковые строки-ключи.
Разделение ключей позволяет сохранить семантику обычного __dict__, но не повторять его структурную часть для каждого объекта. Это особенно полезно для приложений с большим количеством небольших однотипных объектов.
Предположим, сервис создает миллионы объектов одного класса с одинаковыми атрибутами. Даже если значения атрибутов невелики, отдельные словари экземпляров несут заметные накладные расходы: таблицы, служебные поля, ссылки на ключи и резерв емкости.
Неверно считать, что одинаковые экземпляры автоматически занимают одинаково мало памяти. Если часть объектов получает уникальные атрибуты или создается с различающимися наборами полей, словари могут перестать эффективно использовать общую структуру. Поэтому оптимизацию нужно подтверждать профилированием на реальных данных.
У словаря экземпляра можно концептуально разделить две части: таблицу ключей и массив значений. Для экземпляров одного класса таблица с именами атрибутов может быть общей, тогда как позиция каждого значения в массиве соответствует конкретному ключу.
Например, если у всех экземпляров есть атрибуты name и status, строки с этими именами и служебная структура ключей не обязаны храниться заново в каждом __dict__. Каждый экземпляр хранит собственные ссылки на значения name и status.
Такой словарь часто называют split table dictionary. Обычный словарь, в котором ключи и значения находятся в одной структуре, называют combined table dictionary. Переключение между представлениями зависит от операций над словарем и деталей версии CPython.
Разделение не означает, что значения тоже становятся общими. Изменение obj.status меняет значение только у данного объекта. Общей является структура ключей, а не состояние экземпляров.
Практические ограничения:
sys.getsizeof не следует использовать как единственное доказательство экономии: он не дает полноценной картины памяти связанных структур;__slots__ может дать еще меньшую память, но меняет модель хранения атрибутов и ограничивает динамическое добавление полей, если __dict__ явно не предусмотрен.Таким образом, key-sharing dictionaries — автоматическая оптимизация типичного случая, а не замена проектированию компактной модели данных. При жестких требованиях к памяти иногда лучше хранить поля в слотах, массивах или специализированных структурах, принимая связанные с этим ограничения.
В обработчике событий создавались миллионы объектов одного класса. Почти все имели одинаковые поля, но около одного процента событий получали дополнительный диагностический атрибут с большим текстом. Память процесса росла сильнее, чем ожидалось.
Рассматривались три варианта. Оставить обычный __dict__ было проще всего, но это сохраняло накладные расходы и зависело от характера динамических полей. Перевести класс на __slots__ могло заметно уменьшить память, однако потребовало явно описать поля и отдельно решить, где хранить редкие диагностические данные. Хранить обязательные поля в компактных массивах было наиболее экономно, но усложняло код и доступ к данным.
Выбрали слоты для обязательных полей, а диагностические сведения вынесли в отдельное хранилище, индексированное идентификатором события. Это устранило необходимость держать редкий большой атрибут внутри каждого объекта и сделало потребление памяти более предсказуемым. Выбор подтвердили сравнением снимков памяти и измерением полного жизненного цикла объектов, а не только размера одного экземпляра.
Нет. Разделяется прежде всего структура ключей: имена атрибутов и связанная с ними организация доступа. Значения, такие как ссылки на строки, числа или вложенные объекты, принадлежат конкретному экземпляру. Поэтому изменение атрибута одного объекта не изменяет такой же атрибут другого.
__dict__?Нет. Это типичный путь оптимизации, но не обещание языка Python. На представление влияют порядок и способ добавления атрибутов, динамические изменения экземпляров, версия CPython и конкретные операции над словарем. Кроме того, наследование и смешанный набор полей могут сделать структуру менее однородной.
Потому что объявление полей помогает разделяемой структуре, но не устраняет память самих значений и объектов, на которые они ссылаются. Если поля содержат большие строки, коллекции или дублирующиеся данные, основная память может находиться именно там. Нужно отдельно анализировать накладные расходы экземпляров, размер значений, время жизни объектов и стоимость альтернатив вроде __slots__ или массивного хранения.