Python 小数据池和代码块缓存机制
前言
当前测试版本:Python3.7.5;代码前的>>>符号,表示代码输入环境是在CMD中,没有就是编辑器中输入。现在我们先来看几段实验代码:
>>> a1 = 1000
>>> b1 = 1000
>>> a1 is b1 # False
a1 = 1000
b1 = 1000
a1 is b1 # True
>>> a2 = 100
>>> b2 = 100
>>> a2 is b2 # True
>>> a3 = 100.0
>>> b3 = 100.0
>>> a3 is b3 # False
>>> s1 = 'a_b_c'
>>> s2 = 'a_b_c'
>>> s1 is s2 # True
>>> s3 = 'a b_c'
>>> s4 = 'a b_c'
>>> s3 is s4 # False
s5 = 'a b_c'
s6 = 'a b_c'
s5 is s6 # True
先就这样吧,不知道你看到的时候有没有困惑?反正我无意用is输出来的时候是感到困惑的,这里主要有三个概念需要了解:代码块缓存机制、小整数对象池和字符串驻留机制。了解了这三个基本上就很容易理解了。
1.代码块的缓存机制
Python 程序是由代码块构造的。块是一个 Python 程序的文本,它是作为一个单元执行的。
代码块:一个模块, 一个函数, 一个类, 一个文件等都是一个代码块;
交互方式输入的每一条指令都是一个代码块,如CMD中;
Python 在执行同一个代码块的初始化对象的命令时,会检查其值是否存在,如果存在,会将其重用;若满足代码块的缓存机制,则它们在内存中只存在一个,即:id相同。你可以理解为:代码块跟作用域差不多,不同的代码块有不同的作用域。同一个作用域中如果有多个相同的值,编译的时候值就只会保存一次,但多次引用。这样的话,is自然判断为同一个对象。
代码块的缓存机制的适用范围: int(float),str,bool。
int(float): 任何数字在同一代码块下都会复用;
bool: True 和 False 在字典中会以 1,0 方式存在,并且复用;无论你创建多少个变量指向 True,False,它在内存中都只存在一个。
str:同一代码块中,值相同的字符串在内存中只存在一个:
>>> a1 = 1000
>>> b1 = 1000
>>> a1 is b1 # False; a1和b1相当于在两个代码块中,并没有被缓存
a1 = 1000
b1 = 1000
a1 is b1 # True; 被缓存了
f1 = 100.0
f2 = 100.0
print(f1 is f2) # True
s1 = 'janes@!#*ewq'
s2 = 'janes@!#*ewq'
print(s1 is s2) # True
2.小整数对象池
为避免整数频繁申请和销毁内存空间,Python 使用了小整数对象池,Python 对小整数的定义是 [-5, 256] ,这些整数对象是提前建立好的,不会被垃圾回收。
一个 Python 程序中,无论这个整数处于 LEGB 中哪个位置,所有位于这个范围内的整数使用的都是同一个对象。小整数对象池针对的不同代码块之间的缓存机制。
a = -5
b = -5
a is b # True
a = -6
b = -6
a is b # False
a = 256
b = 256
a is b # True
a = 257
b = 257
a is b # Flase
>>> a = 100
>>> b = 100
>>> a is b # True; 虽然不在同一代码块中, 但是它们适用小数据池机制
>>> a3 = 100.0
>>> b3 = 100.0
>>> a3 is b3 # False; 浮点数不算哦
3.字符串驻留机制
Python 解释器为了提高字符串使用的效率和使用性能,编译时,使用了 intern(字符串驻留)技术来提高字符串效率。
即相同值的字符串对象仅仅会保存一份,放在一个字符串储蓄池中,是共用的,当然,肯定不能改变,这也决定了字符串必须是不可变对象 。
字符串驻留发生在程序编译时;
长度为0与1的字符串一定会被驻留;
被驻留的字符串必须由 ASCll字母a-z+A-Z,数字0-9以及下划线_组成。
>>> s1 = '@'
>>> s2 = '@'
>>> s1 is s2 # True; 长度为1
>>> s3 = ''
>>> s4 = ''
>>> s3 is s4 # True; 长度为0
>>> s5 = 'a_b_c'
>>> s6 = 'a_b_c'
>>> s5 is s6 # True
>>> s5 = 'a b_c'
>>> s6 = 'a b_c'
>>> s5 is s6 # False; 空格不满足字符串驻留
>>> a, b = "some_thing!", "some_thing!"
>>> a is b # False; !不满足字符串驻留
>>> a, b = "some_thing", "some_thing"
>>> a is b # True
>>> s7 = 'abd_d23' * 3
>>> s8 = 'abd_d23' * 3
>>> s7 is s8 # True
>>> 'kz' + 'c' is 'kzc' # True; 'kz' + 'c' 编译时已经变成 'kzc'
>>> s1 = 'kz'
>>> s2 = 'kzc'
>>> s1+'c' is 'kzc' # False; s1 + 'c' 中 s1 是变量,会在运行时进行拼接,所以没有被intern
>>> s1 = "a" * 21
>>> s2 = "a" * 21
>>> s1 is s2 # True; 看部分文章说字符串长度超过20不会驻留,我这边倒是还没遇到过
实现 Intern 保留机制的方式非常简单,就是通过维护一个字符串储蓄池,这个池子是一个字典结构,编译时,如果字符串已经存在于池子中就不再去创建新的字符串,直接返回之前创建好的字符串对象,如果之前还没有加入到该池子中,则先构造一个字符串对象,并把这个对象加入到池子中去,方便下一次获取。
但是,解释器内部对intern 机制的使用策略是有考究的,有些场景会自动使用 intern ,有些地方需要通过手动驻留的方式才能启动 ----- 还没遇到。
from sys import intern
>>> x = intern('hello world!!')
>>> y = intern('hello world!!')
>>> x is y # True; 需要两个字符串都使用intern函数驻留,因为只有一个使用的话,另一个并不会主动去与储蓄池中的比较,会直接再创建一个新的字符串
4.试一试
a1 = 1000
b1 = 1000
a1 is b1 # True
f1 = 100.0
f2 = 100.0
print(f1 is f2) # True; 这里可不是CMD中哦
class C1(object):
f = 10.0
a = 100
b = 100
c = 1000
d = 1000
s = 'skj'
class C2(object):
f = 10.0
a = 100
c = 1000
s = 'skj'
print(C1.s is C2.s) # True -- 字符串驻留
print(C1.a is C1.b) # True -- 代码块缓存机制
print(C1.a is C2.a) # True -- 小整数对象池
print(C1.c is C1.d) # True -- 代码块缓存
print(C1.c is C2.c) # False
print(C1.f is C2.f) # False
5.优缺点
优点:值相同的字符串的(比如标识符),直接从池里拿来用,避免频繁的创建和销毁,提升效率,节约内存;
缺点:拼接字符串、对字符串修改之类的影响性能;
因为是不可变的,所以对字符串修改不是 inplace 就地操作,要新建对象,这也是为什么拼接多字符串的时候不建议用 + 而用 join();
join() 是先计算出所有字符串的长度,然后一一拷贝,只 new 一次对象;
6.总结:
如果是同一代码块下,则考虑同一代码块下的缓存机制。如果是不同代码块,则考虑小整数对象池和字符串的驻留机制。需要注意的是,交互式输入时,每个命令都是一个代码块。
代码块缓存机制的适用范围: int,float,str,bool。
长度为0与1的字符串一定会被驻留;字符串驻留发生在程序编译时;不同代码块中,被驻留的字符串必须由 ASCll字母a-z+A-Z,数字0-9以及下划线_组成。
参考:
https://www.pianshen.com/article/9128116263/ python-小数据池,代码块深入剖析
浙公网安备 33010602011771号