在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

100 bytes saved!

但另一方面,使用Python 2.7.12时,同样的调用会返回:

>>> sys.getsizeof(vars(f))
280
>>> sys.getsizeof(dict(vars(f)))
280

100 bytes saved.

在这两种情况下,字典显然都有exactly the same contents个:

>>> vars(f) == dict(vars(f))
True

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

这是怎么回事?为什么在Python3中,一个实例的__dict__大小如此之小?

推荐答案

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会导致组合字典没有这些好处.


顺便说一句,因为这很有趣,我们总是可以打破这种优化.我目前发现了两种方法,一种是愚蠢的方法,另一种是更合理的方案:

  1. 愚蠢的是:

    >>> 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 所有内存增益.

  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
    

    在类的实例中插入不同的键最终会导致拆分表合并.这不仅适用于已经创建的实例;从该类创建的所有consequent个实例都将有一个组合字典,而不是拆分字典.

    # after running previous snippet
    >>> getsizeof(vars(Foo(100, 200)))
    288
    

当然,除了为了好玩,没有什么好理由故意这么做.


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

Python-3.x相关问答推荐

只有在Chrome尚未打开的情况下,打开Chrome后,PySimpleGUI窗口才会崩溃

在循环中使用Print&S结束参数时出现奇怪的问题

基于另一个数据帧计算总和

类变量的Python子类被视为类方法

以某种方式分割字符串

在 Python 中比较和排序列之间的值(带有不匹配列)

如何将 WebDriver 传输到导入的测试?

将水平堆叠的数据排列成垂直

如何确保 GCP Document AI 模型输出与输入文件同名的 JSON?

将 rgb numpy 图像转换为 rgb 列表和相应的索引值

SMTP 库 Python3:不太安全的应用程序访问

Pandas 值列中列表中元素的计数

如何准确测定cv2的结果.在BW/黑白图像中查找对象?

Python:获取未绑定的类方法

具有函数值的 Python 3 枚举

解包时是否可以指定默认值?

ValueError:找不到子字符串,我做错了什么?

multiprocessing.Queue 中的 ctx 参数

如何在多核上运行 Keras?

0 是 0 == 0(#evaluates 为真?)