在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__
的大小如此之小?
答案 0 :(得分:26)
实例__dict__
的实现方式与正常情况相同[&1;}。使用dict
或{}
创建的词典。实例的字典共享键和哈希值,并为不同的部分保留单独的数组:值。 sys.getsizeof
仅在计算实例字典的大小时计算这些值。
CPython中的字典,从Python 3.3开始,以两种形式之一实现:
me_value
member of the PyDictKeyEntry
struct)。据我所知,此表单用于使用dict
,{}
和模块命名空间创建的词典。ma_values
of PyDictObject
)实例字典始终以分组表格形式(密钥共享字典)实现,它允许给定类的实例共享其__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中描述)
顺便说一句,既然它很有趣,我们总能打破这种优化。我目前发现的目前有两种方式,一种愚蠢的方式或一种更明智的方案:
愚蠢:
>>> f = Foo(20, 30)
>>> getsizeof(vars(f))
96
>>> vars(f).update({1:1}) # add a non-string key
>>> getsizeof(vars(f))
288
拆分表仅支持字符串键,添加非字符串键(确实使零感觉)会破坏此规则,CPython会将拆分表转换为合并后的表,从而减少所有内存增益。
可能发生的情况:
>>> 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
当然,除了有趣之外,没有充分理由这样做。
如果有人想知道,Python 3.6的字典实现并没有改变这个事实。前面提到的两种词典形式仍然可用(dict.__sizeof__
的实现也发生了变化,因此getsizeof
返回的值会出现一些差异。)