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

Posted

技术标签:

【中文标题】为啥在 Python 3 中实例的 __dict__ 的大小要小得多?【英文标题】:Why is the __dict__ of instances so much smaller in size in Python 3?为什么在 Python 3 中实例的 __dict__ 的大小要小得多? 【发布时间】:2017-07-14 03:02:44 【问题描述】:

在 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】:

简而言之

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

还有一点

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

组合字典:字典的所有值都存储在每个条目的键和散列旁边。 (me_value member of the PyDictKeyEntry struct)。据我所知,此表单用于使用dict 和模块命名空间创建的字典。 拆分表:值单独存储在一个数组中,而键和哈希是共享的 (Values stored in ma_values of PyDictObject)

实例字典总是以拆分表形式(键共享字典)实现,它允许给定类的实例共享其__dict__ 的键(和散列),并且仅对应的值不同。

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

The implementation of __sizeof__ for 字典考虑了这一事实,并且在计算拆分字典的大小时仅考虑与 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 返回的值应该会有所不同。)

【讨论】:

重要信息 - 可以在 pythonland 中使用那些密钥共享字典吗?

以上是关于为啥在 Python 3 中实例的 __dict__ 的大小要小得多?的主要内容,如果未能解决你的问题,请参考以下文章

Python类的__dict__

python __dict__

python 类变量和实例变量

python中dir(),__dict__

为啥从 Python 中的对象继承,在不指定父对象时更改 __dict__ 的类型不会? [复制]

python实例属性的显示方法-dir__dict__