写给前端的 Python 教程三(字符串驻留和小整数池)

你好,我是 hockor,上一家我们讲了 Python 中基础数据类型的字符串和数字相关的内容,今天,我们来聊聊 Python 中两个与内存优化相关的有趣机制:字符串驻留 (String Interning)小整数池 (Small Integer Pool)。

字符串驻留机制

什么是字符串驻留?

简单来说,当你创建某些符合特定条件的字符串时,Python 会检查内存中是否已存在一个具有相同值的字符串。如果存在,新的变量名会直接指向这个已存在的内存地址,而不是重新创建一个新的字符串对象。

为什么需要字符串驻留?

  • 节省内存:显而易见,如果大量重复的短字符串只存储一份,内存占用会显著降低。

  • 提高比较效率:当比较两个驻留的字符串是否相等时,Python 可以直接比较它们的内存地址(使用 is 运算符)。地址比较远比逐个字符比较要快得多。

哪些字符串会被驻留?

Python 中字符串驻留的规则比较微妙,但通常包括:

  • 编译时常量字符串:在代码中直接写出来的字符串字面量,例如 s = "hello"。

  • 短字符串:长度较短的字符串更容易被驻留。具体长度阈值可能因 Python 版本和实现而异。

  • 符合标识符规则的字符串:由字母、数字和下划线组成的字符串(例如,用作字典键或对象属性名的字符串)通常会被自动驻留。

  • 显式驻留:可以使用 sys.intern() 函数强制驻留一个字符串。

is 和 == 运算符

在 JS 中一般我们没办法看到值的具体内存地址,一般我们就使用 三等号 判断两个变量是否类型相同/值相同/引用相同。

但是在 Python 中呢,给我们提供了一个 id函数,它可以打印出对象的唯一标识符, 同时 Python 还有个 == 运算符,有点类似于咱们 JS 中的双等号。

所以简单理解,你可以认为 Python 的 == 运算符类似于 js 中的双等号,is 类似于 JS 中的三等号(但是他们本质还是有区别的

简单总结如下:

is 运算符

  • 用于身份比较(identity comparison)
  • 检查两个对象是否是同一个内存对象
  • 比较的是对象的内存地址
  • 可以使用 id() 函数查看对象的内存地址

== 运算符

  • 用于值比较(value comparison)
  • 检查两个对象的值是否相等
  • 比较的是对象的内容

示例代码

这里我们来看如下代码示例(还记得字符串前面的 f 吗,是 f-string 格式化的标识符哦)

python 复制代码
# 示例 1: 字面量字符串
a = "hello_python"
b = "hello_python"
print(f"a: '{a}', id(a): {id(a)}")
print(f"b: '{b}', id(b): {id(b)}")
print(f"a is b: {a is b}") # 通常为 True,因为 "hello_python" 是字面量且符合标识符规则


# 示例 2: 拼接产生的字符串
c = "hello_" + "python" # 编译期可能优化为 "hello_python"
print(f"c: '{c}', id(c): {id(c)}")
print(f"a is c: {a is c}") # 通常也为 True

# 示例 3: 动态创建的字符串
x = "world"
d = "hello_" + x # 运行时拼接
e = "hello_" + x
print(f"d: '{d}', id(d): {id(d)}")
print(f"e: '{e}', id(e): {id(e)}")
print(f"d is e: {d is e}") # 通常为 False,因为 d 和 e 是运行时动态创建的

# 示例 4: 使用 sys.intern() 强制驻留
import sys
f = sys.intern("hello_" + x)
g = sys.intern("hello_" + x)
print(f"f: '{f}', id(f): {id(f)}")
print(f"g: '{g}', id(g): {id(g)}")
print(f"f is g: {f is g}") # True,因为显式驻留

打印结果:

对比JS

从我们前端视角看,JavaScript 引擎通常也会对字符串字面量进行优化,使得相同的字符串字面量指向同一内存位置。这与 Python 的编译时常量字符串驻留有相似之处。然而,JavaScript 并没有像 Python sys.intern() 这样提供给开发者直接控制字符串驻留的明确接口。

对于动态生成的字符串,例如

js 复制代码
let dynStr1 = "a" + "b"; 

let dynStr2 = "a" + "b";,

引擎是否会进行类似的优化是其内部实现细节,不像 Python 那样有明确的规则和工具。

在 Python 中,is 关键字用于比较两个对象的内存地址是否相同(即它们是否是同一个对象),而 == 用于比较两个对象的值是否相等。对于驻留的字符串,is 和 == 都会返回 True。

简单说,如果 x is y 成立的话,x 一定等于 y

小整数池

与字符串驻留类似,Python 为了效率,对一定范围内的整数也进行了特殊处理,预先创建并缓存了它们的对象。这个缓存就是所谓的小整数池。

其原理是 Python 解释器在启动时,会预先创建一定范围内的整数对象。当你代码中使用到这个范围内的整数时,Python 不会创建新的整数对象,而是直接返回缓存中已存在的对象的引用。

预缓存的范围是?

这个范围通常是 [-5, 256] (包含 -5 和 256)。也就是说,从 -5 到 256 的整数,在 Python 中都是单例的。

为什么需要小整数池?

  • 性能提升:这些小整数非常常用,预先缓存可以避免频繁地创建和销毁对象,从而提高性能。

  • 节省内存:同样,对于这些常用整数,只保留一份实例可以节省内存。

代码示例

python 复制代码
# 示例 1: 在小整数池范围内
num1 = 100
num2 = 100
print(f"num1: {num1}, id(num1): {id(num1)}")
print(f"num2: {num2}, id(num2): {id(num2)}")
print(f"num1 is num2: {num1 is num2}") # True

num3 = -5
num4 = -5
print(f"num3: {num3}, id(num3): {id(num3)}")
print(f"num4: {num4}, id(num4): {id(num4)}")
print(f"num3 is num4: {num3 is num4}") # True

num5 = 256
num6 = 256
print(f"num5: {num5}, id(num5): {id(num5)}")
print(f"num6: {num6}, id(num6): {id(num6)}")
print(f"num5 is num6: {num5 is num6}") # True

# 示例 2: 超出小整数池范围
num7 = 257
num8 = 257
print(f"num7: {num7}, id(num7): {id(num7)}")
print(f"num8: {num8}, id(num8): {id(num8)}")
print(f"num7 is num8: {num7 is num8}") # 通常为 False (但在某些交互式环境或特定优化下可能为True)

num9 = -6
num10 = -6
print(f"num9: {num9}, id(num9): {id(num9)}")
print(f"num10: {num10}, id(num10): {id(num10)}")
print(f"num9 is num10: {num9 is num10}") # 通常为 False

# 注意:对于超出范围的整数,在某些Python解释器或IDE的交互式环境中,
# 为了优化,可能会对同一行或紧邻行定义的相同数字进行缓存,导致 `is` 返回 True。
# 但在脚本执行时,超出 [-5, 256] 范围的整数通常不会是同一个对象。
a = 1000
b = 1000
print(f"a=1000, b=1000, a is b: {a is b}") # 通常为 False

# 但如果这样写,有些解释器会优化:
c, d = 1000, 1000
print(f"c=1000, d=1000 (同一行), c is d: {c is d}") # 可能为 True

运行结果:

对比JS

JavaScript 中的数字类型 (Number) 遵循 IEEE 754 双精度浮点数标准。它不像 Python 那样有一个明确定义的、语言规范层面的"小整数池"。

js 复制代码
let n1 = 100;
let n2 = 100;
console.log(n1 === n2); // true (值比较)


// JavaScript 中没有 `is` 这样的身份运算符来直接比较原始值的内存地址
// (因为原始值不是对象,它们的值直接存储在变量访问的位置)。
// 对于对象,`===` 比较的是引用。
let obj1 = { val: 100 };
let obj2 = { val: 100 };
let obj3 = obj1;
console.log(obj1 === obj2); // false
console.log(obj1 === obj3); // true

虽然 JavaScript 语言本身没有小整数池,但现代 JavaScript 引擎(如 V8)内部会有很多优化。例如,V8 引擎对小整数(SMIs - Small Integers)有特殊的、更高效的表示和处理方式,以提升性能。但这属于引擎的内部实现细节,对开发者来说是不透明的,不像 Python 的小整数池那样是一个可观察和依赖的语言特性。

总结

今天我们主要是讲到了 Python 的字符串驻留机制和小整数池,是为了优化性能和内存使用而采用的两种策略。

  • 字符串驻留:通过共享不可变字符串的内存,减少内存占用并加快比较速度。

  • 小整数池:预先缓存常用的小整数对象(通常是 -5 到 256),避免重复创建,提升效率。

在 Python 中,过度依赖 is 来比较字符串或数字的值(尤其是超出小整数池范围的数字)可能导致意外的行为。理解其背后的原理能帮助避免这类错误。始终记住,== 用于值比较,is 用于身份比较。

以上是我们今天的内容,我们下一节再见👋

相关推荐
寻星探路2 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
想用offer打牌3 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX4 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
ValhallaCoder4 小时前
hot100-二叉树I
数据结构·python·算法·二叉树
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法5 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate