一个奇怪的特性

这个学期学校又重新开始了 python 的学习,我大一的时候就自学过 python 所以就在自己看书复习快忘光的知识,在看书过程中发现了一个奇怪的特性,在交互式编程框中输入:

a=1
b=1
a is b # true
# is 判断两个常量是否指向同一个对象

书中说 CPython 创建一个 int 型对象,并使 a b 两常量都指向同一个 int 型对象,难道 python 中的对象都是复用的吗,这可是和其他语言完全不同的特性。可是书中接下来又甩出一段代码:

a=10.1
b=10.1
a is b  #false

这样创建的 a,b 又是指向了两个不同的 float 型对象,难道这种规则还和数据类型有关系吗,接下来我又尝试了如下几种数据格式:

image

从图片中可以看到,int、str 复用了相同的对象,而list、float 并没有,对象的复用规则果然和数据类型有关系,是什么机制导致了这种现象呢,在查找资料的过程中我接触了到这样一个名词 「对象池

对象池

整数

简单来说 python 中存在着一个「小整数对象池」,所有 [-5,256] 区间内的小整数都在这里面,当有小整数对象创建的时候,会先到这个池中看看有没有,如果存在的话会直接指向这个已经存在的对象,所以就会发生上述代码中 a,b 指向同一个 1 对象的现象。

字符串

在上述代码截图中可以看到,字符串对象和整数对象存在着一样的行为,那么是不是也有一个 「字符串对象池」呢,非也。
关于字符串,python解释器的处理机制并不一样,其使用的是 intern(字符串滞留) 机制,简单说就是维护一个字典,这个字典维护已经创建字符串(key)和它的字符串对象的地址(value),每次创建字符串对象都会和这个字典比较,没有就创建,重复了就用指针进行引用就可以了。而又只有长度小于 20 的字符串会调用这个机制,实验截图:

image

编译单元

既然讨论到了常量对象的存储机制,就得涉及到「编译单元

背景知识

CPython的代码的“编译单元”是函数——每个函数单独编译,得到的结果是一个PyFunctionObject对象,其中带有字节码、常量池等各种信息。Python的顶层代码也被看作一个函数。

编译单元的区别

关于交互式解释器和 python源码文件解释单元的区别引用知乎用户 @RednaxelaFX 的回答

在CPython的交互式解释器中,每输入一行可以立即执行的代码,Python就会把它当作一个编译单元来编译到字节码并解释执行;如果输入的代码尚未构成一个完整的单元,例如函数声明或者类声明,则等到获得了完整单元的输入后再当作一个编译单元来处理。

当使用python命令去整体解释一个Python源码文件时,编译单元就是函数,Python的顶层代码也被看作一个函数。

编译单元的常量池

每个编译单元都有自己独有的常量池,在同一个编译单元(PyFunctionObject)里出现的值相同的常量,只会在常量池里出现一份,一定会对应到运行时的同一个对象。

而在不同的编译单元中,指向问题就交由给上边两种机制进行处理,小整数池和 intern 机制使得不同编译单元的常量也可能指向同一个对象。

这样一来在一份单独 .py 文件中运行下列代码就会出现看似和上文矛盾的地方

# Test.py
a=10.1
b=10.1
print(a is b) # true

其实这就是因为 a b 都处于顶层代码,属于同一个编译单元,所以共享同一个常量池,而上文的代码是在交互式解释器中执行的,两个赋值语句处于两个两行是不同的编译单元,所以不能共享。

所以:

>>>a=10.1;b=10.1;
>>>a is b
True

赋值语句处于同一行,是在同一个编译单元中,指向同一个对象。

总结

至此,关于常量对象储存机制的问题就全部搞定了,在同一编译单元中,共享常量池,不同编译单元中使用小整数池和 intern 机制。感觉还挺复杂,不过这是作为学习来进行分析,在实际开发中根据根据直觉来使用也不会有什么问题。值得一提的是,此文所说的共享对象全部是指的不可变对象,比如字符串,整数,小数,而列表( list ),字典(dist)等可变对象是不存在共享对象一说,每次新建一个列表都必定会在内存中新建一个列表对象。


Home


AtomG
50 声望1 粉丝