Ответ 1
Короче:
Экземпляры __dict__
реализованы иначе, чем "нормальные" словари, созданные с помощью dict
или {}
. Словари экземпляра разделяют ключи и хэши и сохраняют отдельный массив для частей, которые отличаются: значения. sys.getsizeof
учитывает только эти значения при вычислении размера для экземпляра dict.
Немного больше:
Словари в CPython, как и Python 3.3, реализованы в одной из двух форм:
- Комбинированный словарь. Все значения словаря хранятся вместе с ключом и хешем для каждой записи. (
me_value
членPyDictKeyEntry
struct). Насколько мне известно, эта форма используется для словарей, созданных с помощьюdict
,{}
и пространства имен модулей. - Разделить таблицу. Значения хранятся отдельно в массиве, а ключи и хеши разделяются ( Значения, хранящиеся в
ma_values
PyDictObject
)
Экземпляры экземпляров всегда реализуются в форме разделенных таблиц (словарь обмена ключами), который позволяет экземплярам данного класса делиться ключами (и хэшами) для их __dict__
и только отличаться соответствующими значениями.
Все это описано в PEP 412 - Словарь по обмену ключами. Реализация для раскодированного словаря помещается в Python 3.3
, поэтому предыдущие версии семейства 3
, а также Python 2.x
не имеют этой реализации.
Реализация __sizeof__
для словарей учитывает этот факт и учитывает только размер, соответствующий массиву значений при вычислении размер для разделенного словаря.
Это, к счастью, самоочевидно:
Py_ssize_t size, res;
size = DK_SIZE(mp->ma_keys);
res = _PyObject_SIZE(Py_TYPE(mp));
if (mp->ma_values) /*Add the values to the result*/
res += size * sizeof(PyObject*);
/* If the dictionary is split, the keys portion is accounted-for
in the type object. */
if (mp->ma_keys->dk_refcnt == 1) /* Add keys/hashes size to res */
res += sizeof(PyDictKeysObject) + (size-1) * sizeof(PyDictKeyEntry);
return res;
Насколько я знаю, словари с разделительными таблицами создаются только для пространства имен экземпляров, использование dict()
или {}
(как описано в PEP) всегда приводит к объединенному словарю, который не имеет этих преимущества.
В стороне, так как это весело, мы всегда можем нарушить эту оптимизацию. В настоящее время я обнаружил два текущих способа, глупый способ или более разумный сценарий:
-
Быть глупым:
>>> f = Foo(20, 30) >>> getsizeof(vars(f)) 96 >>> vars(f).update({1:1}) # add a non-string key >>> getsizeof(vars(f)) 288
Разделенные таблицы поддерживают только строковые ключи, добавление нестрочного ключа (что действительно делает смысл ноль) нарушает это правило, а CPython превращает таблицу split в комбинированную, теряя все выгоды от памяти.
-
Возможный сценарий:
>>> f1, f2 = Foo(20, 30), Foo(30, 40) >>> for i, j in enumerate([f1, f2]): ... setattr(j, 'i'+str(i), i) ... print(getsizeof(vars(j))) 96 288
Различные ключи, вставленные в экземпляры класса, в конечном итоге приводят к объединению таблицы split. Это не относится только к уже созданным экземплярам; все последующие экземпляры, созданные из класса, будут иметь комбинированный словарь, а не разделенный.
# after running previous snippet >>> getsizeof(vars(Foo(100, 200))) 288
конечно, нет веских оснований, кроме забавы, для этого специально.
Если кто-то блуждает, реализация словаря Python 3.6 не изменяет этот факт. Две вышеупомянутые формы словарей в то время как все еще доступны, просто уплотнены (реализация dict.__sizeof__
также изменилась, поэтому некоторые значения должны появиться в значениях, возвращаемых из getsizeof
.)