为什么Python 3中的__dict__实例的大小要小得多?

时间:2017-02-23 14:48:58

标签: python python-3.x class dictionary python-internals

在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。

那么,这里发生了什么?为什么Python 3中实例的__dict__的大小如此之小?

1 个答案:

答案 0 :(得分:26)

简而言之

实例__dict__的实现方式与正常情况相同[&1;}。使用dict{}创建的词典。实例的字典共享键和哈希值,并为不同的部分保留单独的数组:值。 sys.getsizeof仅在计算实例字典的大小时计算这些值。

多一点

CPython中的字典,从Python 3.3开始,以两种形式之一实现:

实例字典始终以分组表格形式(密钥共享字典)实现,它允许给定类的实例共享其__dict__的密钥(和哈希)并且只在相应的值上有所不同。

PEP 412 -- Key-Sharing Dictionary中对此进行了描述。拆分字典的实现落在Python 3.3中,因此3系列的早期版本以及Python 2.x都没有这种实现。

字典的

The implementation of __sizeof__考虑​​了这个事实,并且在计算拆分字典的大小时只考虑与values数组对应的大小。

谢天谢地,不言自明:

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中描述)

仅为实例名称空间创建分组表词典< em>总是会产生一个没有这些好处的组合字典。

顺便说一句,既然它很有趣,我们总能打破这种优化。我目前发现的目前有两种方式,一种愚蠢的方式或一种更明智的方案:

  1. 愚蠢:

    >>> f = Foo(20, 30)
    >>> getsizeof(vars(f))
    96
    >>> vars(f).update({1:1})  # add a non-string key
    >>> getsizeof(vars(f))
    288
    

    拆分表仅支持字符串键,添加非字符串键(确实使感觉)会破坏此规则,CPython会将拆分表转换为合并后的表,从而减少所有内存增益。

  2. 可能发生的情况:

    >>> 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
    

    在类的实例中插入不同的键最终会导致拆分表合并。这不仅适用于已创建的实例;从类创建的所有结果实例将具有组合字典而不是拆分字典。

    # after running previous snippet
    >>> getsizeof(vars(Foo(100, 200)))
    288
    
  3. 当然,除了有趣之外,没有充分理由这样做。

    如果有人想知道,Python 3.6的字典实现并没有改变这个事实。前面提到的两种词典形式仍然可用(dict.__sizeof__的实现也发生了变化,因此getsizeof返回的值会出现一些差异。)