之前分析过浮点数的创建,而整数的创建与之是类似的,都是基于字面量形式的 C 数据创建 Python 数据。
- 创建浮点数可以使用 PyFloat_FromDouble、PyFloat_FromString 等;
- 创建整数可以使用 PyLong_FromLong、PyLong_FromDouble、PyLong_FromString 等;
创建整数的这些函数直接去 Objects/longobject.c 里面查看即可,这里就不多说了,我们重点来看一下小整数对象池。
我们知道整数属于不可变对象,运算之后会创建新的对象。
>>> a = 666
>>> id(a)
140078521506224
>>> a += 1
>>> id(a)
140078521506096
>>>
显然这种做法一定存在性能缺陷,因为程序运行时会有大量的对象创建和销毁。根据浮点数的经验,我们猜测 Python 应该也对整数使用了缓存池吧。答案是差不多,只不过不是缓存池,而是小整数对象池。
一些使用频率高的整数在创建之后,会被保存在一个静态数组里面,我们称之为小整数对象池。
看一下小整数对象池的实现。
// Objects/longobject.c
#define NSMALLPOSINTS 257
#define NSMALLNEGINTS 5
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
数组 small_ints 便是小整数对象池,它是一个类型为 PyLongObject、长度为 262 的数组,里面缓存了 -5 到 256 之间的整数。当然这只是解释器的默认行为,因为 -5 到 256 之间的整数使用频率最高,但你也可以根据自身情况修改源码,让它缓存更多的整数,以提升效率,当然这也会额外占用一些内存。
小整数对象池是预先创建好的,里面的整数全局唯一,我们来验证一下。
>>> n1 = 256
>>> n2 = 256
>>> id(n1), id(n2)
(140078528922016, 140078528922016)
>>>
>>> n1 = 257
>>> n2 = 257
>>> id(n1), id(n2)
(140078521507472, 140078521506704)
>>>
256 位于小整数对象池内,所以全局唯一,需要使用的话直接去取即可,因此它们的地址是一样的。但 257 不在小整数对象池内,所以它们的地址不一样。
另外上面的代码是交互式执行的,但如果有小伙伴不是通过交互式,那么打印地址的时候会得到出乎意料的结果。
a = 257
b = 257
print(id(a) == id(b)) # True
可能有人会好奇,为什么地址又是一样的了,257 明明不在小整数对象池中啊。虽然涉及到了后面的内容,但提前解释一下也是可以的。主要区别就在于一个是交互式执行的,另一个是通过 python3 xxx.py 的方式执行的。
首先 Python 的编译单元是函数,每个函数都有自己的作用域,在这个作用域中出现的所有常量都是唯一的,并且都位于常量池中,由 co_consts 指向。虽然上面的对象不在函数中,而是在全局作用域中,但全局也可以看成是一个函数,它也是一个独立的编译单元。同一个编译单元中,相同的常量只会出现一次。
当执行 a = 257 的时候,会创建 257 这个整数,并放入常量池中。所以 b = 257 的时候就不会再创建了,因为常量池中已经有了,所以会直接从常量池中获取,因此它们的地址是一样的,因为是同一个 PyLongObject。
而对于交互式环境来说,因为输入一行代码就会立即执行一行,所以任何一行可独立执行的代码都是一个独立的编译单元。注意:是可独立执行的代码,比如变量赋值、函数、方法调用等等。但如果是 if、for、while、def 等需要多行表示的逻辑,比如 if 2 > 1:,显然就不是一行可独立执行的代码,它还依赖你输入的下面的内容。
# 此时按下回车,我们看到不再是 >>>,而是 ...
# 这代表还没有结束,还需要你输入下面的内容
>>> if 2 > 1:
... print("2 > 1")
... # 此时这个 if 语句整体才是一个独立的编译单元
2 > 1
>>>
但是像 a = 1、foo()、lst.appned(123) 这些显然是一行可独立执行的代码,因此在交互式中它们是独立的编译单元。
# 此时这行代码已经执行了,它是一个独立的编译单元
>>> a = 257
# 这行代码也是独立的编译单元,所以它里面的常量池为空,因此要重新创建 257
>>> b = 257
# 由于它们是不同常量池内的整数,所以 id 是不一样的
>>> id(a), id(b)
(140078521506768, 140078521506096)
再来看个例子。
>>> a = 666; b = 666
>>> id(a), id(b)
(140078521506512, 140078521506512)
>>>
>>> a, b = 777, 777
>>> id(a), id(b)
(140078521506224, 140078521506224)
666 和 777 明显不在常量池中,为啥 a 和 b 指向对象的地址是一样的呢?相信你能猜到原因,因为上面两种方式无论哪一种,都是在同一行,因此整体会作为一个编译单元。既然是同一个编译单元,那么常量池里面的每个常量只会创建一次,所以地址是一样的。
注意:常量池里面的常量一定在编译期间就可以确定,比如整数、浮点数、字符串等等,解释器在编译期间会静态收集起来,保存在常量池中。但如果编译期间无法确定,就不会静态收集了,举个例子。
>>> a, b = int("257"), int("257")
>>> id(a), id(b)
(140078521506768, 140078521506512)
>>>
>>> a, b = int("123"), int("123")
>>> id(a), id(b)
(140078528917760, 140078528917760)
>>>
>>> a, b = 258, 258
>>> id(a), id(b)
(140078521507632, 140078521507632)
int("257") 需要在运行时执行,因此会创建两个 257,然后赋值给 a 和 b,所以地址不一样。
int("123") 虽然也在运行时执行,但解释器创建完之后发现 123 位于小整数对象池中,于是会直接从池子里面取,所以打印的地址一样。注意:int("123") 肯定是会执行的,只是执行之后发现结果存在于小整数对象池中,会再将创建的对象销毁。
258 属于编译期间可以静态收集的常量,会被保存在常量池中,而常量池里的常量只会出现一次,所以打印的地址一样。
⭐️:常量不仅仅是 123、"hello" 这种,像 2 ** 10、"AAA" + "BBB" 这种也属于常量,它们在编译之后会被替换为 1024、"AAABBB",这个过程被称为常量折叠。
以上就是小整数对象池相关的内容,比较简单,因为小整数对象池就是一个数组,里面缓存了 -5 到 256 之间的 Python 整数,而这些整数可以直接拿来用。
下一篇文章我们来分析整数的运算,这也是最关键的地方。
欢迎大家关注我的公众号:古明地觉的编程教室。
如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。