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-小数据池,代码块深入剖析

posted @ 2021-04-06 19:39  应当如次  阅读(280)  评论(0)    收藏  举报
回到顶部