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

Jim*_*ard 16 python dictionary class python-3.x python-internals

在Python中,与包含该类相同属性的字典相比,为类实例创建的字典很小:

import sys

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

f = Foo(20, 30)
Run Code Online (Sandbox Code Playgroud)

使用Python 3.5.2时,以下调用getsizeof生成:

>>> sys.getsizeof(vars(f))  # vars gets obj.__dict__
96 
>>> sys.getsizeof(dict(vars(f))
288
Run Code Online (Sandbox Code Playgroud)

288 - 96 = 192 字节已保存!

但是,另一方面,使用Python 2.7.12,相同的调用返回:

>>> sys.getsizeof(vars(f))
280
>>> sys.getsizeof(dict(vars(f)))
280
Run Code Online (Sandbox Code Playgroud)

0 保存的字节数

在这两种情况下,词典显然具有完全相同的内容:

>>> vars(f) == dict(vars(f))
True
Run Code Online (Sandbox Code Playgroud)

所以这不是一个因素.此外,这也仅适用于Python 3.

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

Jim*_*ard 26

简而言之:

实例__dict__的实现方式与使用dict或创建的"普通"词典不同{}.实例的字典共享键和散列,并为不同的部分保留单独的数组:值.sys.getsizeof仅在计算实例dict的大小时计算这些值.

多一点:

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

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

这在PEP 412-密钥共享字典中都有描述.拆分字典的实现落在Python中,3.3因此该3系列的先前版本以及Python 2.x没有此实现.

__sizeof__for dictionaries 的实现考虑了这一事实,并且在计算拆分字典的大小时仅考虑与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;
Run Code Online (Sandbox Code Playgroud)

据我所知,只为实例的命名空间创建了分表表字典,使用dict(){}(如PEP中所述)总是会产生一个没有这些好处的组合字典.


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

  1. 傻:

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

    拆分表只支持字符串键,添加非字符串键(实际上没有任何意义)会破坏此规则,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
    
    Run Code Online (Sandbox Code Playgroud)

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

    # after running previous snippet
    >>> getsizeof(vars(Foo(100, 200)))
    288
    
    Run Code Online (Sandbox Code Playgroud)

当然,除了有趣之外,没有充分理由这样做.


如果有人想知道,Python 3.6的字典实现并没有改变这个事实.上述两种形式的词典虽然仍然可用,但只是进一步压缩了(实现dict.__sizeof__也发生了变化,因此在返回的值中会出现一些差异getsizeof.)