引例
1 |
|
上述C代码在运行会报错,这是由于C语言的int整型位数位32bit,表示范围有限32 位的有符号整数_C++中整数的范围(-2^31 ~ 2^31-1,-2147483648~2147483647)
,显然上述代码超出了这个表示范围,因此报错。
相比之下python中却不会出现上述的问题,并且能够正常表示数值。
1 | i = 10000000000000000000000000000000000000000 |
更夸张的例子,计算10的100次方
1 | print(10**1000) |
为什么python能做到这一点呢? 这是本节探讨的主题:int对象,永不溢出的整数。
PyLongObject
为了解答上述问题,我们需要从python中int类型对象的设计出发:
python中int对象PyLongObject
定义位置:/objects/longobject.h
1 | typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */ |
跟踪 _longobject
来到/include/intrepr.h
1 | struct _longobject { |
可以看到整型对象PyLongObject
根据其对象头部PyObject_VAR_HEAD
可以看到是属于变长对象,其次PyLongObject
的值是由数组存放的digit ob_digit[1]
显然到这里会好奇digit
究竟是什么,结果同文件下发现定义,这里只关注相关内容:
1 |
|
从源码得知digit
就是一个C语言无符号整数,只是根据位数不同声明的长度不同而已,编译python解析器时可以通过相关宏定义PYLONG_BITS_IN_DIGIT
与PYLONG_BITS_IN_DIGIT
来选择使用的digit
版本,但默认使用PYLONG_BITS_IN_DIGIT == 30
,指示digit
数组的长度
至此,我们得知了python利用整型数组来实现更大整数的表示与运算的
这里我们假设digit
的元素个数由n,那么python中的int整型对象的布局大致如下:
大整数的布局
大整数分为正数
、负数
、零
,python据此规定不同整数在int对象中的存储方式,总结有以下三点:
- 整数
绝对值
根据值的大小具体情况分为若干部分,保存于ob_digit
数组中 ob_digit
数组长度保存于PyVarObject.ob_size
字段中,对于负整数的情况,PyVarObject.ob_size
为负,即ob_size
字段不仅表示需要几个数组元素存放int值的绝对值,还表示正负- 整数
0
以PyVarObject.ob_size
=0来表示,ob_digit
数组为空
实例说明:
2 ** 30
是由于上文提到的宏定义PYLONG_BITS_IN_DIGIT
相关,为什么不32bit全都使用呢?
这一点其实跟加法进位
有关。如果使用全32位用来保存绝对值,那么为了保证加法不溢出(因为加法可能产生进位),需要先强转32位类型为64位,然后进行计算。但牺牲最高1位之后,加法运算便不用担心加法溢出了。
那么为什么多牺牲1位的空间呢?
专栏主认为是为了和digit 只有16位的情况整合起来,当digit数组位数有16位,则python使用15位。下面是我自己的示意图(个人理解)
小整数静态对象池
前面学习过python对对象进行了分类,分为:变长对象 & 定长对象
、可变对象 & 不可变对象
前面对变长对象 定长对象
讨论的比较多,可能有的读者都忘记了可变对象 & 不可变对象
的类别了,这里重复一下:
不可变对象
,对象在完成初始化后不可做值的修改,强行变动会改变变量名的指向对象。以本节说明的int对象为例,它就是不可变对象:1
2
3
4
5
6
7a = 1
print(id(a))
a += 1
print(id(a))
#---------------output---------------#
4402723168
4402723200可以看到,对不可变对象做修改,会导致变量名指向的对象发生变化
可变对象
,以list为例子:1
2
3
4
5
6
7l = [1,2,3]
print(id(l))
l.append(4)
print(id(l))
#---------------output---------------#
140253038051904
140253038051904对可变对象做修改,会导致变量名指向的对象不发生变化
显然对于这类操作是相当频繁的,这会给运行带来极大的开销(一旦不可变对象发生变动,则需要进行新对象的创建,使相对应的变量名指向新的对象,旧的对象若没有被引用还涉及旧对象的销毁!)
那么Python为啥做这种不明智的设计呢?这就不得不提 —- 小整数静态对象池
这是python为解决上述提到的缺陷的方案:预先将常用的整数对象创建好,以备使用,这就是小整数对象池,很简单对吧!相关代码(位置/Objects/longobject.c):
1 |
NSMALLPOSINTS
宏规定了对象池正整数个数(包括0),个数=257NSMALLNEGINTS
宏规定了对象池负整数个数,个数 = 5
按照默认配置,python启动后静态创建一个包含262个元素的整数数组并一次初始化为负数集合[-5,-1],[0]以及[1,256]
。相关文章
It turns out Python keeps an array of integer objects for “all integers between -5 and 256”. When we create an int in that range, we’re actually just getting a reference to the existing object in memory.
If we set x = 42, we are actually performing a search in the integer block for the value in the range -5 to +257. Once x falls out of the scope of this range, it will be garbage collected (destroyed) and be an entirely different object. The process of creating a new integer object and then destroying it immediately creates a lot of useless calculation cycles, so Python preallocated a range of commonly used integers.
精彩面试题
1 | a = 1 |
其实a, b指向的都是小整数静态对象池中的对象。
回顾
- python的整数对象是
变长 & 不可变对象
能够串联多个C整数类型,实现大整数表示。整数对象的关键字段包括存放对象绝对值的ob_digit
数组,存放串联个数以及符号的ob_size
- 整数绝对值根据
PYLONG_BITS_IN_DIGIT
指定的ob_digit
的数组元素长度进行存放 - 整数0,digit数组为空,ob_size字段为0
- 另外,小整数缓冲池优化了
不可变对象
所带来开销大的问题