注:本文不区分作为编程语言的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\n",a,b); //value should be the same
a = PyInt_FromLong(257);
b = PyInt_FromLong(257);
printf("a=%p,b=%p\n",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

您知道是为什么吗?


以上内容转载自 知乎

Python中神秘的-5到256的更多相关文章

  1. Python中的xxx == xx是否等价于xxx is xxx

    先上代码: >>> a = 1 >>> b = 1 >>> a is b True >>> a == b True what? ...

  2. python 中的unicode详解

    通过例子来看问题是比较容易懂的. 首先来看,下面这个是我新建的一个txt文件,名字叫做ivan_utf8.txt,然后里面随便编辑了一些东西. 然后来用控制台打开这个文件,同样也是截图: 这里就是简单 ...

  3. Python中is和==的区别

    Python中有很多运算符,今天我们就来讲讲is和==两种运算符在应用上的本质区别是什么. 在讲is和==这两种运算符区别之前,首先要知道Python中对象包含的三个基本要素,分别是:id(身份标识) ...

  4. 【循序渐进学Python】3. Python中的序列——字符串

    字符串是零个或多个的字符所组成的序列,字符串是Python内建的6种序列之一,在Python中字符串是不可变的. 1. 格式化字符串 字符串格式化使用字符串格式化操作符即百分号%来实现.在%左侧放置一 ...

  5. python中的is、==和cmp()比较字符串

    python 中的is.==和cmp(),比较字符串 经常写 shell 脚本知道,字符串判断可以用 =,!= 数字的判断是 -eq,-ne 等,但是 Python 确不是这样子地.所以作为慢慢要转换 ...

  6. 8.python中的数字

    python中数字对象的创建如下, a = 123 b = 1.23 c = 1+1j 可以直接输入数字,然后赋值给变量. 同样也可是使用类的方式: a = int(123) b = float(1. ...

  7. Python中的字符串处理

    Python转义字符 在需要在字符中使用特殊字符时,python用反斜杠(\)转义字符.如下表: 转义字符 描述 \(在行尾时) 续行符 \\ 反斜杠符号 \' 单引号 \" 双引号 \a ...

  8. python中的subprocess.Popen()使用

    python中的subprocess.Popen()使用 从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回 ...

  9. Python中星号的本质和使用方式

    翻译:Python 开发者 - 一汀, 英文:Trey Hunner http://blog.jobbole.com/114655/ Python开发者 在 Python 中有很多地方可以看到*和** ...

随机推荐

  1. 用python实现网络文件共享

    第一步:打开命令行 第二步:切换到要共享的文件夹目录 第三步:运行命令 python -m http.server 端口号 然后在浏览器输入显示的路径即可访问文件夹下的文件,点击即可下载,手机端亦可访 ...

  2. 用 async/await 来处理异步(转)

    昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简洁,同时async/await 已经被标准化,是时候学习一下了. 先说一下async的用法,它作为一个 ...

  3. jumpserver sudo 权限控制模板

    sudo 权限控制,常用 ALL,!/bin/bash,!/bin/tcsh,!/bin/su,!/usr/bin/passwd,!/usr/bin/passwd root,!/bin/vim /et ...

  4. maven的安装与使用(运行单元测试和打包等)

    maven的下载与安装 maven是用于java的自动化构建工具. 1.下载: http://maven.apache.org/download.cgi 下载maven包,比如 apache-mave ...

  5. LoadRunner通过验证参数判断事物的成功与失败

    if(atoi(lr_eval_string("{Param_DiscountID}")) > 0){ //lr_message("多机多酒:%s",lr ...

  6. 解密国内BAT等大厂前端技术体系-美团点评之下篇(长文建议收藏)

    引言 在上篇中,我已经介绍了美团点评的业务情况.大前端的技术体系,其中大前端的技术全景图如下: 上篇重点介绍了工程化和代码质量的部分,工程化涵盖了客户端持续集成平台-MCI.全端监控平台-CAT.移动 ...

  7. 排序算法之希尔排序的python实现

    希尔排序(Shell’s Sort)是插入排序的一种,是直接插入排序算法的一种更高版本的改进版本. 希尔排序的工作原理 如下: (1)把记录按步长gap分组,对每组记录采用直接插入排序方法进行排序: ...

  8. C语言学习建议!8年编程开发经验

    C语言是几乎所有编程语言的先驱与灵感的来源,Perl,PHP,Python和Ruby都是用它写的,同样什么Microsoft Windows,Mac OS X,还有GNU/Linu这些操作系统,都是靠 ...

  9. 你了解真正的 restful API 吗?

    本文原创地址,博客:https://jsbintask.cn/2019/03/20/api/restful-api-best-practices/(食用效果最佳),转载请注明出处! 前言 在以前,一个 ...

  10. netty(三)---NioEventLoop分析

    问题 : NioEventLoop 作用到底是什么?是在哪里用到的? NioEventLoop 和我们开头创建的 ServerBootstrap 和 EventLoopGroup 是什么关系 ? Ni ...