文头白话:
作为一个菜鸟,为了避免误导我就不说Python源码结构了。和那一堆源码文件初次见面就很头疼,我对C语言了解不深,看源码也只是为了更好的了解Python,所以可能会有很多错误或者不足的地方。如果有大神看到了,能帮忙指出,将感激不尽。
1. Python整数的表示
首先要知道的是在python3中整型数字均采用long类型实现的,那么我们就要找到相关文件。在Objects路径下能找到longobject.c文件,查看代码不难发现PyLongObject应该就是整数对应的结构体。
在longobject.h这个头文件中找到了它 ,然后将它的内容拿出来
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
PyObject_VAR_HEAD还是看不懂的,那就继续看看它是个什么东西。
这些找到之后,就将PyObject_VAR_HEAD拆开了
struct _longobject {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
digit ob_digit[1];
};
这样以后发现PyObject应该还是一个结构体,Py_ssize_t,digit 我们也找到它们对应的我们认识的类型
struct _longobject {
int ob_refcnt;
struct _typeobject *ob_type;
int ob_size;
digit ob_digit[1];
};
ob_refcnt:引用计数的,用于垃圾回收;
*ob_type:类型结构体指针,指向一个类型对象,用于表达类型信息;
ob_size:用于指明ob_digit的长度
ob_digit:存储的就是我们说的整型数字
那么他具体是如何存储的呢?下面就一点点揭开它的面纱。首先,要看看digit是这什么类型,它在longintrepr.h中
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit; //这里uint32_t对应的是,unsigned int
...
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit;
...
#endif
查看源码中的解释发现,这里区分32位机和64位机。那64位机PYLONG_BITS_IN_DIGIT就是30,digit就会被定义为无符号长整形,如果是32位机 digit就会被定义为无符号短整型。这是后我们再回到上面那个结构体_longobject 其中ob_digit 就是一个无符号长/短整型数组。
那么数字又是如何存到ob_digit里面的呢? 为了方便理解也方便解释,我们假设ob_digit每一个元素只占8个比特位,因为每个元素都是无符号数所以8个比特位最大可以表示255。据此,我们举几个例子来说明:
// 下面都将十进制数字以每8个比特位的二进制数字相加表示
1 ->2进制:00000001
2 ->2进制:00000010
...
255->2进制:11111111
256->2进制:11111111 00000001
257->2进制:11111111 00000010
// 因为我们以8位举例,这里ob_digit中应该是255进制数从低位到高位存放,其他位数同理计算。
1 = [1]
2 = [2]
...
255 = [255]
256 = [1, 1]
257 = [2, 1]
现在大概可以看明白整数的存储了吧。但是上面并不是真实情况,我们只是以8个比特位来举例子,那么回到我们上面说的64位机上digit被定义为unsigned int,那么C语言中int占4个字节就是32个比特位。那在ob_digit中肯定是不能将32个比特位占用完的,这个我们后面再说。
这样正整数是可以表示了,但是负数的问题还没有解决,这时候我们回到上面拆解的_longobject结构体中看一个整型变量 ob_size 它后面有句注释 /* Number of items in variable part */ 说明它表示的是ob_digit中元素个数,我们也拆解过了,ob_size对应的类型是有符号整型,那么我们ob_digit中表示的数字符号跟ob_size符号保持一致即可。
2.小整数池问题
之前跟伙伴们也探讨过这个问题。当时的问题大概是这样子的:
// 猜测打印结果a和b,c和d是否会一致
a = 1
b = 1
print(id(a))
print(id(b))
c = 1000
d = 1000
print(id(c))
print(id(d))
按照当时的认知,python每定义一个不可变类型数据均会新开辟一个空间存放变量,那很明显应该都是不相同的。但是执行结果很意外,如下:
我们发现a和b是一样的,c和d是不一样的。 在编程中像-1,0,1,2,3,4...这样比较小的数字用的是很频繁的,每次使用都要去开辟空间,释放空间,虽然每一个耗费资源不大,但是顶不住量大啊!所以python中设计了小整数池的概念。那么我们就继续查看源码(longobject.c):
这里我们先关注一下这两个数字
这里有一段预处理代码:这里看来#if NSMALLNEGINTS + NSMALLPOSINTS > 0是会成立的,这里就定义了一个静态的数组small_ints。然后一但认定要定义的整数是小整数池中的,就会通过下面的方法,取到该整数的指针。
从上面的代码中"assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);"可以看出小整数池的范围应该是-5到256。不同版本可能不尽相同,我这个是python3.7.6
3.python整数占用内存大小
要想真正知道python中整数大小,是要了解C语言结构体的内存开辟方式的。我这里就以我粗浅的了解简单说一下(可能不对,如果有大佬看到错误,帮忙指出来,将万分感谢)
首先结构体开辟空间有一个内存对齐原则:1)结构体大小必须是占用空间最大成员的字节的整数倍;2)每一个成员的起始地址必须是自身字节数的整数倍;3)好像还跟编译器有关,就不多说了。
依据上述原则我们来看一下我们的整数对应结构体是怎样开辟空间的:
struct _longobject {
int ob_refcnt;
struct _typeobject *ob_type;
int ob_size;
digit ob_digit[1];
};
首先,我们先把上面拆解的整数结构体拿出来。我们要知道指针类型根据机器不同可能是4个字节或者8个字节,这里我们就以4个字节为例。
接下来就剩一个ob_digit了,因为它是一个可变长的,故整型数字大小的空间就应该是前边成员占用空间总和如上图,加上ob_digit占用空间整数倍大小。
结语:
关于python整型的解读暂时说到这里,未尽之处后面继续补充。与君共勉!