从 CPython 文档中澄清“应该可以更改 1 的值”

22 python implementation cpython

请参阅此链接:https : //docs.python.org/3/c-api/long.html#c.PyLong_FromLong

当前的实现为 -5 到 256 之间的所有整数保留了一个整数对象数组;当您在该范围内创建一个 int 时,您实际上只是取回了对现有对象的引用。所以,应该可以改变 1 的值。我怀疑 Python 的行为,在这种情况下,是 undefined。:-)

在这种情况下,粗线是什么意思?

Tob*_*ias 33

这意味着 Python 中的整数是具有“值”字段的实际对象,用于保存整数的值。在 Java 中,你可以像这样表达 Python 的整数(当然,省略了很多细节):

class PyInteger {

    private int value;

    public PyInteger(int val) {
        this.value = val;
    }

    public PyInteger __add__(PyInteger other) {
        return new PyInteger(this.value + other.value);
    }
}
Run Code Online (Sandbox Code Playgroud)

为了避免大量具有相同值的 Python 整数,它会缓存一些整数,如下所示:

PyInteger[] cache = {
  new PyInteger(0),
  new PyInteger(1),
  new PyInteger(2),
  ...
}
Run Code Online (Sandbox Code Playgroud)

但是,如果你做这样的事情会发生什么(让我们value暂时忽略它是私有的):

PyInteger one = cache[1];  // the PyInteger representing 1
one.value = 3;
Run Code Online (Sandbox Code Playgroud)

突然之间,每次1在程序中使用时,您实际上都会返回3,因为表示的对象1的有效值为3

事实上,你可以在 Python 中做到这一点!也就是说:可以在 Python 中更改整数的有效数值。在这篇 reddit 帖子中有一个答案。不过,为了完整起见,我将其复制到此处(原始学分转到Veedrac):

import ctypes

def deref(addr, typ):
    return ctypes.cast(addr, ctypes.POINTER(typ))

deref(id(29), ctypes.c_int)[6] = 100
#>>> 

29
#>>> 100

29 ** 0.5
#>>> 10.0
Run Code Online (Sandbox Code Playgroud)

Python 规范本身并没有说明如何在内部存储或表示整数。它也没有说明应该缓存哪些整数,或者应该缓存任何整数。简而言之:Python 规范中没有任何内容定义如果你做这样愚蠢的事情会发生什么;-)。


我们甚至可以走得更远......

实际上,value上面的字段实际上是一个整数数组,模拟任意大整数值(对于 64 位整数,您只需组合两个 32 位字段等)。然而,当整数开始变大并超过标准的 32 位整数时,缓存不再是一个可行的选择。即使你使用了字典,比较整数数组的相等性也会带来太多的开销而收益太少。

您实际上可以通过使用is来比较身份来自己检查:

>>> 3 * 4 is 12
True
>>> 300 * 400 is 120000
False
>>> 300 * 400 == 120000
True
Run Code Online (Sandbox Code Playgroud)

在典型的 Python 系统中,只有一个对象表示数字12120000,另一方面,几乎从未被缓存。因此,上面300 * 400产生了一个表示 的新对象120000,它不同于为右侧数字创建的对象。

为什么这是相关的?如果您更改一个小数字的值,例如129,它将影响使用该数字的所有计算。您很可能会严重破坏您的系统(直到您重新启动)。但是如果你改变一个大整数的值,影响会很小。

更改12to的值13意味着3 * 4将产生13。Chaning的价值120000,以130000有效果少得多,并300 * 400仍然会产生(新的)120000,而不是130000

一旦你考虑到其他 Python 实现,事情就会变得更加难以预测。 例如,MicroPython没有用于小数的对象,但会动态处理它们,而PyPy可能只是优化您的更改。

底线:您修补的数字的确切行为确实未定义,但取决于几个因素和确切的实现。


回答评论中的一个问题:6上面 Veedrac 代码中的意义是什么?

Python 中的所有对象共享一个共同的内存布局。第一个字段是一个引用计数器,它告诉您当前有多少其他对象正在引用此对象。第二个字段是对对象的类型的引用。由于整数没有固定大小,因此第三个字段是数据部分的大小(您可以在此处(通用对象)此处(整数/长整数找到相关定义):

struct longObject {
    native_int      ref_counter;  // offset: +0 / +0
    PyObject*       type;         // offset: +1 / +2
    native_int      size;         // offset: +2 / +4
    unsigned short  value[];      // offset: +3 / +6
}
Run Code Online (Sandbox Code Playgroud)

在32位的系统,native_intPyObject*既占用32位,而在64位的系统上它们占据64位,自然。因此,如果我们ctypes.c_int在 64 位系统上以 32 位(使用)访问数据,则可以在 offset 处找到整数的实际值+6ctypes.c_long另一方面,如果将类型更改为,则偏移量为+3

因为id(x)在 CPython 中返回 的内存地址x,你实际上可以自己检查一下。基于上面的deref函数,让我们做:

struct longObject {
    native_int      ref_counter;  // offset: +0 / +0
    PyObject*       type;         // offset: +1 / +2
    native_int      size;         // offset: +2 / +4
    unsigned short  value[];      // offset: +3 / +6
}
Run Code Online (Sandbox Code Playgroud)

  • @NSR是的,整数被全局缓存,任何此类更改都会影响您在该解释器中所做的一切。 (2认同)