为什么 Python 对小整数的引用计数出奇地高?

Why are Python Ref Counts to small integers surprisingly high?

this 的回答中,我找到了一种获取 Python 中对象引用计数的方法。

他们提到使用 sys.getrefcount()。我试过了,但我得到了意想不到的结果。当有 1 个引用时,计数似乎是 20。这是为什么?

我看了documentation,好像没有说明原因。

在第一次 sys.getrefcount 调用时,该对象恰好有 20 个引用。这不仅仅是您创建的引用;在其他模块和 Python 内部还有对它的各种其他引用,因为(这是一个实现细节)Python 的标准实现只创建一个 100 对象并使用它适用于 Python 程序中出现的所有 100

对一个对象有很多引用有很多原因。追踪哪一个可能很困难,并决定它是否值得可能会绕过你的兴趣水平。调试应用程序和 python 变体的开发人员最感兴趣的是引用计数。

  • Python 试图为每个引用保留一个实际值。因此,示例中的 100 与递归调用的某些内部限制相同的 100 或与当前循环索引相同的 100。
  • Python 保留对一些常见对象的额外引用,包括低整数。 1,234,567 的引用计数与 20 的引用计数不同。
  • 许多函数记忆并保留对最近参数的引用。
  • 一些口译员保留对最近的值和最近行中引用的值的引用。例如,前面的return值存储在“_”中。这意味着解释器中的 运行 和命令行中的 运行 将给出不同的答案。
  • 像所有引用计数方案一样,也有错误。例如,PyTuple_GetItem() 有一些有问题的选择。

确切的引用计数和这些计数的含义在 PyPi 与 C-Python 与 IPython 中是不同的。引用计数很少是在 Python.

中查找奇怪行为的好工具

阅读 Python2.7 的源代码很有趣,它写得非常好并且易​​于阅读。 (如果你想在家里一起玩,我指的是版本 2.7.12。)理解代码的一个很好的起点是优秀的系列讲座:C Python Internals 从初学者的角度开始。

与我们相关的关键代码(用 C 编写)出现在文件 'Objects/intobject.c' 中(为了清楚起见,我删除了一些 #ifdef 代码并稍微修改了新 Integer 对象的创建):

    #define NSMALLPOSINTS           257
    #define NSMALLNEGINTS           5
    static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

    PyObject *
    PyInt_FromLong(long ival)
    {
        register PyIntObject *v;
        if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
            v = small_ints[ival + NSMALLNEGINTS];
            Py_INCREF(v);
            return (PyObject *) v;
        }
        /* Inline PyObject_New */
        v = (PyIntObject *)Py_TYPE(v);
        PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        return (PyObject *) v;
    }

所以本质上它创建了一个预设数组,其中包含从 -5 到 256 的所有数字,并在可能的情况下使用这些对象(使用 Py_INCREF 宏增加它们的引用计数)。如果不是,它将创建一个全新的 PyInt_Type 对象,该对象初始化为 1 的引用计数。

为什么每个数字的引用计数似乎都是 3(实际上几乎所有新对象),只有当您查看 Python 生成的字节码时才能揭开谜团。虚拟机使用值堆栈(有点像 Forth 中的)运行,每次将对象放入值堆栈时,它都会增加引用计数。

所以我怀疑发生的事情是你的代码本身提供了你看到的所有 3 个引用,因为对于不在小值列表中的数字,你应该得到一个唯一的对象。第一个引用大概在调用 getrefcount 的调用者的值堆栈中;第二个在 getrefcount 框架的局部变量列表中;第三个可能在 getrefcount 框架中的值堆栈上,同时查找其引用计数。

如果您想进一步深入研究问题,一个有用的工具是 'compile' 命令和 'dis' 模块中的 'dis' 命令(反汇编),它将在一起允许您读取任何一段 Python 代码生成的实际字节代码,并且应该可以帮助您准确地找出创建第三个引用的时间和位置。

至于小值的较高引用计数,当您启动 Python 时,它会自动加载整个标准库并在您开始之前运行大量 Python 模块初始化代码解释你自己的代码。这些模块拥有自己的许多小整数副本(以及也是唯一的 None 对象)。