Подтвердить что ты не робот

Почему __dict__ экземпляров, размер которых намного меньше в Python 3?

В Python словари, созданные для экземпляров класса, являются крошечными по сравнению со словарями, содержащими те же атрибуты этого класса:

import sys

class Foo(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

f = Foo(20, 30)

При использовании Python 3.5.2 следующие вызовы getsizeof производят:

>>> sys.getsizeof(vars(f))  # vars gets obj.__dict__
96 
>>> sys.getsizeof(dict(vars(f))
288

288 - 96 = 192 сохранены байты!

Используя Python 2.7.12, хотя, с другой стороны, одни и те же вызовы возвращаются:

>>> sys.getsizeof(vars(f))
280
>>> sys.getsizeof(dict(vars(f)))
280

0 сохранены байты.

В обоих случаях словари, очевидно, имеют точно такое же содержимое:

>>> vars(f) == dict(vars(f))
True

так что это не фактор. Кроме того, это также относится только к Python 3.

Итак, что здесь происходит? Почему размер __dict__ экземпляра настолько мал в Python 3?

4b9b3361

Ответ 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.)