In short:
实例__dict__
的实现方式不同于使用dict
或{}
创建的"普通"词典.实例share的字典、键和散列以及保留一个单独的数组,用于不同的部分:值.sys.getsizeof
在计算实例dict的大小时只计算这些值.
A bit more:
从Python 3.3开始,CPython中的字典以以下两种形式之一实现:
实例字典是以拆分表的形式(密钥共享字典)实现的,它允许给定类的实例共享其__dict__
的密钥(和散列),并且只在相应的值上有所不同.
这些都在PEP 412 -- Key-Sharing Dictionary中描述.split dictionary的实现是在Python 3.3
中实现的,因此3
系列的早期版本以及Python 2.x
都没有这种实现.
The implementation of __sizeof__
for dictionary在计算拆分字典的大小时会考虑这一事实,并且只考虑与值数组相对应的大小.
谢天谢地,这是不言自明的:
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;
据我所知,拆分表字典是created only for the namespace of instances,使用dict()
或{}
(也在PEP中描述)always会导致组合字典没有这些好处.
顺便说一句,因为这很有趣,我们总是可以打破这种优化.我目前发现了两种方法,一种是愚蠢的方法,另一种是更合理的方案:
愚蠢的是:
>>> f = Foo(20, 30)
>>> getsizeof(vars(f))
96
>>> vars(f).update({1:1}) # add a non-string key
>>> getsizeof(vars(f))
288
拆分表只支持字符串键,添加一个非字符串键(这真的有zero个意义)打破了这一规则,CPython将拆分表转换为一个组合表,从而失go 所有内存增益.
可能发生的情况:
>>> 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
在类的实例中插入不同的键最终会导致拆分表合并.这不仅适用于已经创建的实例;从该类创建的所有consequent个实例都将有一个组合字典,而不是拆分字典.
# after running previous snippet
>>> getsizeof(vars(Foo(100, 200)))
288
当然,除了为了好玩,没有什么好理由故意这么做.
如果有人想知道,Python 3.6的字典实现并没有改变这一事实.上述两种形式的词典虽然仍然可用,但只是进一步压缩了(dict.__sizeof__
的实现也发生了变化,因此getsizeof
返回的值应该会出现一些差异.)