在 Python 的世界中,性能优化并不仅仅依赖开发者编写高效的代码。CPython(用 C 语言实现的 Python 解释器,最常用版本)也在底层对常见对象进行了精妙的缓存优化。这些优化手段不但提高了性能,还减少了内存占用。
一、小整数缓存机制
CPython 在启动时会预先创建并缓存 -5 ~ 256 范围内的整数对象。这些对象在整个运行期间都是单例,也就是说多个变量引用它们时,会共享同一块内存。
a = 100b = 100print(a is b) # True
为什么是这个范围?这主要是出于性能考虑。小整数在实际应用中频繁出现,如循环计数、列表索引、状态标识等,缓存可以显著提高执行效率。
CPython 源码(Objects/longobject.c)中定义如下:
#define NSMALLPOSINTS 257 // 0 到 256#define NSMALLNEGINTS 5 // -1 到 -5
对于超出这个范围的整数(如 1000),每次创建的对象是不同的:
a = 1000b = 1000print(a is b) # False(通常)
注意:
某些解释器(比如交互式解释器、Jupyter Notebook、PyCharm 控制台)可能会因为常量合并优化,使得 a is b 成为 True,这不是标准行为,不可依赖。
二、字符串驻留机制
CPython 对一部分字符串也进行了缓存,称为“字符串驻留”(string interning)。如果多个变量持有的是同一个驻留字符串,它们就会共享内存地址。
哪些字符串会被驻留:
1、所有长度为 0 或 1 的字符串,如:""(空字符串)、"a"(单字符字符串)等。
2、所有只包含字母、数字和下划线的标识符样式字符串(即合法的变量名样式),如:"hello"、"var_1" 等。
标识符样式字符串是在编译阶段自动驻留的。
3、显式调用 sys.intern() 的字符。
a = "hello" # 标识符样式字符串b = "hello"print(a is b) # True (被驻留)
a = "hello world" #含空格或标点的字符串b = "hello world"print(a is b) # False(不是合法的变量名样式,也可能被解释器优化,结果为 True)
a = "a" # 单字符字符串b = "a"print(a is b) # True(长度为 1)
可以使用 sys.intern() 手动强制驻留字符串:
import sys
a = sys.intern("hello world")b = sys.intern("hello world")print(a is b) # True
提示:
字符串驻留的行为在不同 Python 版本之间有所不同,不能完全依赖。
三、单例对象的共享
Python 内置了几个全局唯一的单例对象,它们在内存中只存在一份,并在整个程序中复用。
None,空值。
True / False,布尔值。
... ,Python 内置对象,类型为 EllipsisType,常用作占位符或切片中的缩略表示。
NotImplemented,用于算术操作不支持时的返回值。
a = Noneb = Noneprint(a is b) # True
a = ...b = ...print(a is b) # True
这些对象在 CPython 启动时就已经创建,并且始终只存在一份。
四、空的不可变容器缓存
CPython 还缓存了某些空的不可变对象,例如:
a = ()b = ()print(a is b) # True 因为空元组是缓存的
a = ""b = ""print(a is b) # True 空字符串也是缓存的
这也是一种内存优化手段,因为空的不可变对象经常出现,完全可以共享使用。
要的注意的是,可变对象不会缓存:
print([] is []) # Falseprint({} is {}) # False
五、补充说明
1、不要将空元组(())的行为套用在 frozenset 上,空的 frozenset() 并不会被缓存,即使内容相同,每次创建的对象是不同的。
a = frozenset()b = frozenset()print(a is b) # False
早期 CPython 某些版本/交互式解释器中,有可能因为编译器优化产生缓存效果,但这不是规范行为,不可靠。
2、不要滥用 is 来比较值
is 是用来判断对象身份,而不是比较值。比较数值时应使用 ==。
# 正确 if a == 1000: ...
# 不推荐if a is 1000: ...
使用 is 的正确场景是:比较单例对象(如 None、True、False)。
“点赞有美意,赞赏是鼓励”