【问题标题】:Clarification for "it should be possible to change the value of 1" from the CPython documentation从 CPython 文档中澄清“应该可以更改 1 的值”
【发布时间】:2020-09-23 01:20:14
【问题描述】:

查看此链接:https://docs.python.org/3/c-api/long.html#c.PyLong_FromLong

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

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

【问题讨论】:

    标签: python implementation cpython


    【解决方案1】:

    这意味着 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);
        }
    }
    

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

    PyInteger[] cache = {
      new PyInteger(0),
      new PyInteger(1),
      new PyInteger(2),
      ...
    }
    

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

    PyInteger one = cache[1];  // the PyInteger representing 1
    one.value = 3;
    

    突然之间,每次你在程序中使用1,你实际上都会得到3,因为代表1的对象的有效值是3

    确实,您可以在 Python 中做到这一点!即:可以在 Python 中更改整数的有效数值。 this reddit post 有答案。不过,为了完整起见,我将其复制到此处(原始学分归 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
    

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


    我们甚至可以更进一步......

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

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

    >>> 3 * 4 is 12
    True
    >>> 300 * 400 is 120000
    False
    >>> 300 * 400 == 120000
    True
    

    在典型的 Python 系统中,只有一个对象代表数字 12。另一方面,120000 几乎不会被缓存。因此,在上面,300 * 400 生成了一个新对象,表示 120000,这与为右侧数字创建的对象不同。

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

    12 的值更改为13 意味着3 * 4 将产生13。将120000 的值更改为130000 的影响要小得多,300 * 400 仍会产生(新的)120000 而不是130000

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

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


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

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

    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
    }
    

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

    因为CPython中的id(x)返回的是x的内存地址,这个其实可以自己查。基于上面的deref函数,我们来做:

    >>> deref(id(29), ctypes.c_long)[3]
    29
    >>> deref(id(29), ctypes.c_long)[1]
    10277248
    >>> id(int)       # memory address of class "int"
    10277248
    

    【讨论】:

    • @Toblas 感谢您的出色回答!只是想问一下:如果我在Python脚本中将29更改为100,这是否也意味着相应的更改也会在导入的模块中可见?
    • @NSR 是的,整数是全局缓存的,任何此类更改都会影响您在该解释器中所做的一切。
    【解决方案2】:

    由于对象是通过引用返回的,那么如果您更改对象,程序中的所有内容都会更改。

    因此,以值 1 为例,您可以将其更改为 42。 这只是可能的,因为 C API 让您可以内部访问 Python 解释器;感觉不太可能从 Python 脚本本身中执行此操作(例如,不使用 cffi 之类的东西)。

    【讨论】:

      【解决方案3】:

      另一种思考如果您在内部“将 1 的地址处的值更改为 17”会发生什么的另一种方法是打印range(3) 中的每个元素——您会看到 0、17、2。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-01-28
        • 1970-01-01
        • 2013-09-11
        • 1970-01-01
        • 1970-01-01
        • 2019-12-11
        • 1970-01-01
        相关资源
        最近更新 更多