• Python中神秘的-5到256


    注:本文不区分作为编程语言的Python和作为语言实现的Python。后者均默认为CPython。

    了解他人对Python源代码的掌握情况,我喜欢问这样一个问题

    请问,在Python中,256和257的主要区别是什么?

    我期望的回答是

    Python内部,对这两个数采取了不同的对象创建策略

    1.做一个实验

    我们知道,在一个对象的生存期内,可以用id()函数得到这个对象的唯一标识。即,id返回值相同的对象一定是同一个对象。

    启动Python的交互模式(主流版本的Python 2和Python 3均可),输入以下语句并观察结果。

    >>> a = 0
    >>> b = 0
    >>> id(a) == id(b)
    True
    >>> a = -5
    >>> b = -5
    >>> id(a) == id(b)
    True
    >>> a = -6
    >>> b = -6
    >>> id(a) == id(b)
    False
    >>> a = 256
    >>> b = 256
    >>> id(a) == id(b)
    True
    >>> a = 257
    >>> b = 257
    >>> id(a) == id(b)
    False
    >>> a = -1.0
    >>> b = -1.0
    >>> id(a) == id(b)
    False
    

    你也可以写一个带有for循环的脚本,更加全面的验证这样一个结论:

    Python中,对于整数对象,如果其值处于[-5,256]的闭区间内,则值相同的对象是同一个对象

    您有可能想到了,这也许和Python内部的某种机制有关。让我们更加深入的使用API来验证这个结论。

    2. 使用Python API

    以Python 2为例,可以使用这样的代码得到与Python脚本等价的结论:

    #include <Python.h>
    
    int main(int argc,char ** argv) {
      PyObject *a,*b;
      Py_SetProgramName(argv[0]);
      Py_Initialize();
      a = PyInt_FromLong(256);
      b = PyInt_FromLong(256);
      printf("a=%p,b=%p
    ",a,b); //value should be the same
      a = PyInt_FromLong(257);
      b = PyInt_FromLong(257);
      printf("a=%p,b=%p
    ",a,b);  //value should be different
      Py_Finalize();
      return 0;
    }
    

    如果使用Python3 ,PyInt_FromLong要替换为PyLong_FromLong。

    从运行结果可以看到,从256产生的两个PyObject*,指向了内存中相同的地址,但是从257产生的PyObject则是相互独立的。

    3.没有什么好奇怪的

    为什么是-5到256之间的这200多个数?其实没有什么奇怪的,Python本身就是这样实现的。

    让我们打开源代码一看究竟。首先看看Python2的实现方式。下面的代码是以本文写作时最新的2.7.14为例子的。

    在Python自身的main函数里,会调用Py_Initialize这个函数初始化Python内部的一系列模块。(Modules/main.c,551行)。在初始化过程中,_PyInt_Init会被调用(Python/pythonrun.c,210行)。_PyInt_Init的唯一作用就是初始化small_ints数组(Objects/intobject.c,1452行):

    int
    _PyInt_Init(void)
    {
        PyIntObject *v;
        int ival;
    #if NSMALLNEGINTS + NSMALLPOSINTS > 0
        for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) {
            if (!free_list && (free_list = fill_free_list()) == NULL)
                return 0;
            /* PyObject_New is inlined */
            v = free_list;
            free_list = (PyIntObject *)Py_TYPE(v);
            (void)PyObject_INIT(v, &PyInt_Type);
            v->ob_ival = ival;
            small_ints[ival + NSMALLNEGINTS] = v;
        }
    #endif
        return 1;
    }
    

    我们看到了两个宏:NSMALLNEGINTS 和NSMALLPOSINTS 。在intobject.c的头部找到它们的定义:

    #ifndef NSMALLPOSINTS
    #define NSMALLPOSINTS           257
    #endif
    #ifndef NSMALLNEGINTS
    #define NSMALLNEGINTS           5
    #endif
    

    从_PyInt_Init的实现上,我们可以看到被放入small_ints的数字范围是-5到256。因此,你可以通过修改源代码的方式,将这个范围任意的扩展。

    如果你不对这两个宏进行改动,那么在Python启动的时候,会先创建一个200多PyObject大小的数组,默认的把-5从256的所有整数创建完毕。

    我们知道,Python在遇到诸如 a = 5这样的语句的时候,最终会落到PyInt_FromLong这个函数里。我们看看这个函数是怎么写的(intobject.c,86行):

    PyObject *
    PyInt_FromLong(long ival)
    {
        register PyIntObject *v;
    #if NSMALLNEGINTS + NSMALLPOSINTS > 0
        if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
            v = small_ints[ival + NSMALLNEGINTS];
            Py_INCREF(v);
    #ifdef COUNT_ALLOCS
            if (ival >= 0)
                quick_int_allocs++;
            else
                quick_neg_int_allocs++;
    #endif
            return (PyObject *) v;
        }
    #endif
        if (free_list == NULL) {
            if ((free_list = fill_free_list()) == NULL)
                return NULL;
        }
        /* Inline PyObject_New */
        v = free_list;
        free_list = (PyIntObject *)Py_TYPE(v);
        (void)PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        return (PyObject *) v;
    }
    

    这段代码很容易读懂,首先判断字面量的范围是不是在-5到256之间,如果是,直接从small_ints里面取得缓存的对象,如果不是,再通过PyObject_New来创建一个新的对象。

    Python3 的代码以此类推,相似的代码在longobject.c里面。

    4. 为什么要这样做

    主要还是性能上的考虑。由于创建一个新的对象是比较折腾的:在内存池中分配空间,赋予对象的类别并赋予其初始的值。从-5到256这些小的整数,在Python脚本中使用的非常频繁,又因为他们是不可更改的,因此只创建一次,重复使用就可以了。

    有兴趣的读者可以把负责缓存边界的两个宏改小,或者让它们的和是负数以取消这个功能,看看日常的脚本是否有性能的变化。

    5. 一点扩展……

    考虑这样的命令:

    >>> a,b = 400,400
    >>> id(a) == id(b)
    True
    

    您知道是为什么吗?


    以上内容转载自 知乎

  • 相关阅读:
    mySQL练习题
    JAVA实现C/S结构小程序
    JavaLinkedHashSet练习
    关于Extjs删除分页后删除最后一条数据页面无效的问题。
    hibernate 插入,更新数据库错误
    错误!错误!错误!
    坑爹的oracle
    关于hibernate实体类
    第一个项目的需求分析
    Ueditor 单独使用上传图片及上传附件方法
  • 原文地址:https://www.cnblogs.com/leisurelylicht/p/Python-zhong-shen-mi-de5dao256.html
Copyright © 2020-2023  润新知