为什么 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 对象)。
在 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 对象)。