Python限制字符串驻留范围 仅缓存合法标识符提升寻址性能

【python】你绝对不知道的字符串缓存机制!这个知识点有点太偏了 — 码农高天

码农高天

Python仅自动驻留符合标识符命名规则的字符串以加速内部属性指针比对,由于跨编译周期及特殊字符会打破底层缓存共享,开发者判定字符串相等必须使用==而严禁使用is。

AI-assisted summary of 【python】你绝对不知道的字符串缓存机制!这个知识点有点太偏了 — original source. Check the original for full context.

Source published:

Key ideas

  • Python 字符串作为不可变对象,其底层通过编译期常量折叠、全切片复用、const cache 与运行期字符串驻留(String Interning)等多层机制实现内存共享。 (0:00)
  • 编译阶段会通过常量折叠将 "x" + "y" 直接合成为常量 "xy",而变量拼接(x + y)因生成 BINARY_ADD 指令必须在运行时分配新对象。 (5:00)
  • 运行期自动字符串驻留依赖底层的 all_name_chars 检查,仅对纯由大小写字母、数字和下划线构成的合法标识符生效,带有空格的字符串在跨编译单元(如 exec 或 REPL)时不会自动复用。 (7:30)
  • 因字符串对象复用受底层编译优化与驻留规则严格约束,业务代码中判断字符串相等性应严格使用 ==,切忌依赖 is 指针比较。 (7:30)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 4 月 26 日发布。视频简介指出本期内容将深度探讨 Python 中相对冷门偏门的字符串缓存机制,并调侃称如果观众在看视频之前全都知道这些机制,就无需再看他的视频了。

## TL;DR 视频以一个七种字符串构造方式的对比小测验切入,深入剖析了 Python 针对不可变字符串的底层复用设计。高天通过字节码分析,逐步拆解了全切片原地直返、编译期常量表合并、常量折叠以及跨代码对象的 const cache 优化机制。随后,他进一步解释了在动态执行场景下生效的运行期字符串驻留(String Interning),揭示了其依赖合法标识符检查(all_name_chars)以加速解释器属性查找的核心逻辑。最后,视频强调业务代码中绝对不能依赖字符串缓存的偶然性,比较字符串值必须始终使用 == 而非 is。

## Atmosphere & style 视频整体呈现出硬核硬派、条理分明且极具启发性的极客讲堂风格。开篇的反直觉测验瞬间拉满观众的好奇心与求知欲,随后借助反编译指令与底层 C 逻辑层层抽丝剥茧,带来通透解谜般的顿悟感。主讲人语调从容流畅、技术剖析直击本质,既展现了对 CPython 底层架构的透彻理解,又适时给出了清晰落地的工程实操建议。

## Bulletin - [00:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=0s) Python 字符串作为不可变对象,其底层通过编译期常量折叠、全切片复用、const cache 与运行期字符串驻留(String Interning)等多层机制实现内存共享。 - [05:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=300s) 编译阶段会通过常量折叠将 "x" + "y" 直接合成为常量 "xy",而变量拼接(x + y)因生成 BINARY_ADD 指令必须在运行时分配新对象。 - [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 运行期自动字符串驻留依赖底层的 all_name_chars 检查,仅对纯由大小写字母、数字和下划线构成的合法标识符生效,带有空格的字符串在跨编译单元(如 exec 或 REPL)时不会自动复用。 - [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 因字符串对象复用受底层编译优化与驻留规则严格约束,业务代码中判断字符串相等性应严格使用 ==,切忌依赖 is 指针比较。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=0s) 在纯字母字符串测试中,字面量赋值、全切片、字面量相加、exec 执行与函数返回均可指向同一内存对象,唯独变量相加会产生独立对象。 - [00:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=0s) 字符串的不可变性(immutability)是其能够在多处安全共享同一内存地址、避免深拷贝开销的底层前提。 - [02:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=150s) 对字符串执行起始为 0、结束为末尾的全切片(如 a[:])时,底层直接返回原对象指针,并不会像列表切片那样分配新内存。 - [02:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=150s) 同一作用域内多次书写相同字符串字面量时,编译器仅在代码对象的常量表(co_consts)中记录一次,统一通过 LOAD_CONST 加载复用。 - [05:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=300s) 字面量相加操作("x" + "y")在编译阶段由编译器常量折叠提前计算,不产生任何运行期加法计算开销。 - [05:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=300s) 变量拼接(x + y)无法在静态阶段推导结果,生成的 BINARY_ADD 字节码必须在运行时动态分配内存并生成全新对象。 - [05:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=300s) 编译器内部维护了 const cache 字典,使得跨 code object(如全局作用域与独立函数内部)的相同字面量也能共享常量实例。 - [05:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=300s) Python 编译器的符号缓存统一处理标识符与常量,使得同名的局部变量名、函数名与字面量常量在底层共享同一字符串对象。 - [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) exec() 调用与 REPL 交互式回车属于独立的编译生命周期,先前基于代码文件的编译期缓存机制在此类场景下全部失效。 - [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 运行期字符串驻留(String Interning)机制会在加载 code object 时将符合规则的常量与符号注册到底层全局驻留池中。 - [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 全局驻留设有准入门槛,仅通过 all_name_chars 校验(仅含字母、数字、下划线的合法标识符)的字符串才会被自动全局驻留,含空格字符串则被排除。 - [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 字符串驻留的核心价值在于将 Python 频繁发生的属性与字典键查找从耗时的逐字符 strcmp 比较降维为极速的指针地址比较。 - [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 开发者在任何业务逻辑中比较字符串内容时,必须使用 == 比较值,绝不能使用 is 判定相等。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=0s) 字符串小测验与不可变对象的缓存原理

视频伊始,高天抛出了一个针对 Python 字符串底层机制的测验。他在屏幕左侧展示了一段代码,分别通过七种不同的方式构造出值为 `"xy"` 的字符串变量: 1. 变量 `a` 直接赋值为 `"xy"`; 2. 变量 `b` 通过切片操作 `a[:]` 生成; 3. 变量 `c` 再次直接赋值为 `"xy"`; 4. 变量 `d` 由两个字符串字面量相加拼接而成(`"x" + "y"`); 5. 变量 `e` 由保存了字符串 `"x"` 和 `"y"` 的两个独立变量相加生成; 6. 变量 `f` 通过 `exec("f = 'xy'")` 动态执行代码生成; 7. 变量 `g` 则是调用一个返回 `"xy"` 的函数所得到的返回值。

在 Python 中,如果两个变量的 `id()` 相同,则代表它们在内存中指向同一个对象。面对“这七个变量里究竟有几个会指向同一个对象”的疑问,测试结果令人惊讶:除了通过变量动态相加的 `e` 拥有独立的对象地址之外,其余六个变量竟然全部指向了同一个内存对象。更耐人寻味的是,如果在 `"x"` 与 `"y"` 之间加入一个空格将其变成 `"x y"`,输出结果立刻发生了变化——不仅变量 `e` 依然独立,原本指向同一对象的 `f`(通过 `exec` 生成)也变成了一个全新的对象。高天指出:“在这简单又稍显离谱的现象背后,实际上隐藏了大约四五种 Python 对 String 的缓存机制,而今天我们就要把这些缓存机制一一讲明白。”

从基本特性来看,Python 中的字符串是不可变对象(immutable)。正因为不可变对象的内容无法被篡改,当多个上下文需要使用完全相同的值时,解释器完全可以让它们安全地共享同一块内存,这在字符串被极其频繁使用的 Python 程序中能够节省海量的内存开销。最基础的引用传递便体现了这一点:当把 `a` 赋值给 `b` 时,Python 并不会完整复制一份字符串,而是直接让两个变量指向同一对象。

```mermaid flowchart TD accTitle: Python 字符串 7 种构造方式的对象指向对比 accDescr: 展示了 7 种不同构造方式下字符串对象的内存复用情况,区分了完全复用、编译期折叠与运行时独立分配。

subgraph Inputs["7 种构造方式(以 'xy' 为例)"] A["a = 'xy'"] B["b = a[:]"] C["c = 'xy'"] D["d = 'x' + 'y'"] F["exec('f = xy')"] G["g = func() 返回 'xy'"] E["e = x + y (变量拼接)"] end

subgraph Objects["内存对象指向"] SharedObj["同一内存对象 ('xy')"] NewObj["独立运行时对象 (e)"] end

A --> SharedObj B -->|"全切片返回自身"| SharedObj C -->|"常量复用"| SharedObj D -->|"编译期常量折叠"| SharedObj F -->|"运行期标识符驻留"| SharedObj G -->|"编译期 Const Cache"| SharedObj E -->|"运行时 BINARY_ADD"| NewObj ```

- **测验结果对比**:对于纯字母字符串 `"xy"`,只有变量 `e`(变量相加)拥有不同的 `id`,其余 6 个变量共享同一个对象;而一旦带有空格变成 `"x y"`,`e` 和 `f`(`exec` 生成)均指向独立对象。 - **不可变性与内存复用**:不可变类型无需深复制即可安全共享指针,这是 Python 各层字符串缓存机制的存在基石。

---

[02:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=150s) 全切片直返自身与编译期常量复用

为了拆解上述现象背后的具体机制,高天首先解释了切片操作 `a[:]` 为什么没有产生新对象。在开发者的直觉中,切片往往伴随着复制;例如对一个列表执行 `list[:]` 会触发浅拷贝并产生新列表。但对字符串执行子串截取(substring)时,解释器内部存在一个专门的优化逻辑:“当你做 substring 这个操作的时候,如果你的起始点是 0、结束点是最后,它就会直接返回本身这个没有改变过的 String。”因此,看似执行了切片复制,底层其实仅仅是把原始对象的指针直接传递出来。

接着,高天转向了字面量重复赋值的机制:为什么在代码中分别书写 `a = "xy"` 和 `c = "xy"`,它们不会创建两个分离的字符串对象?高天借助 `dis` 模块分析了生成的 Python 字节码。在 Python 代码编译生成代码对象(code object)的过程中,编译器会对当前作用域内的所有常量进行收集,将提取到的字符串 `"xy"` 作为常量存入该代码对象的常量表 `co_consts` 中。当后续代码再次遇到完全相同的字面量时,编译器生成的字节码指令依然是加载同一张常量表中的对应项(如 `LOAD_CONST 0`),随后分别执行 `STORE_NAME a` 和 `STORE_NAME c`。这意味着在代码编译阶段,Python 就已经确定这两个变量绑定的是同一个编译期常量。

- **全切片优化**:字符串切片若覆盖从 `0` 到结尾的完整区间,直接返回原字符串对象本身,绝不分配新内存。 - **列表与字符串的切片差异**:列表执行 `l[:]` 会强制分配新的列表对象,而字符串切片因不可变特性享有就地复用优化。 - **字节码级常量载入**:多次书写相同字符串字面量在同一个编译单元内只会在 `co_consts` 记录一次,统一通过 `LOAD_CONST` 索引加载。

---

[05:00](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=300s) 常量折叠与编译期 Const Cache 跨作用域优化

紧接着,高天探讨了字面量拼接(`d = "x" + "y"`)与变量拼接(`e = x + y`)在行为上的巨大差异。反编译结果显示,`"x" + "y"` 编译出的字节码与直接赋值为 `"xy"` 完全一致,中间根本没有字符串加法操作。这是因为 Python 编译器内建了常量折叠(constant folding)优化:在编译阶段,如果表达式的各个操作数都是可以直接计算的字面常量,编译器就会提前算出结果 `"xy"` 并写入常量表。相反,当计算 `x + y` 时,编译器无法在静态阶段断定变量的实际运行值,字节码中必须生成明确的 `BINARY_ADD` 指令;该指令在运行时动态分配内存并创建新字符串,因而其对象地址必然与其他常量不同。

更深一层的机制体现在跨作用域的常量共享上:为什么外部全局变量 `a = "xy"` 与一个局部函数 `def func(): return "xy"` 的返回值能够共享同一个对象?从常理推断,函数体会被编译为一个独立的 code object,理论上拥有各自独立的常量表。高天解释道,这是因为 Python 编译器内部维护着一个名为 `const cache` 的常量缓存字典:“也就是说在 Python 编译代码的时候,每当他需要用一个新的常量的时候,他就会先回头找一找之前是不是也用到了同样的常量,如果之前用到了,它就会让这个新的常量直接指向之前已经开辟的那个常量。”这种机制避免了在为每个 code object 分配私有常量表时产生冗余的内存分配。

更为巧妙的是,code object 内部的常量表与变量名池复用了同一套符号缓存逻辑。高天在现场做了一个趣味拓展:在一个函数内部声明一个名为 `xy` 的局部变量,再声明一个名为 `xy` 的函数,最后在外层定义字符串常量 `"xy"`。检查这三者所持有的底层字符串会发现,存储局部变量名的字符串、存储函数名的字符串以及字符串字面量常量,最终在内存中全都指向同一个底层对象。

- **常量折叠(Constant Folding)**:字面量计算(如 `"x" + "y"`)在编译阶段由编译器提前求值,不消耗运行时计算开销。 - **运行时加法(BINARY_ADD)**:涉及变量引用的加法操作必须在运行时动态评估,因此每次都会分配新的独立对象。 - **编译期常量缓存(const cache)**:同一编译会话中跨 code object 共享字面常量,杜绝重复创建相同的常量实例。 - **标识符与常量的统一缓存**:Python 底层将变量名、函数名与字面量常量在编译符号缓存中统一处理,同名标识符与常量底层共享同一字符串对象。

---

[07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 运行期字符串驻留(String Interning)与标识符限制

最后,高天解析了最违背直觉的现象:为什么在使用 `exec()` 或 Python 交互式命令行(REPL)时,`"xy"` 的引用一致,而带有空格的 `"x y"` 却分道扬镳?高天指出,`exec()` 接收字符串参数并在运行时动态触发全新的编译,交互式解释器(REPL)中每敲击一次回车也是一次独立的编译过程。由于编译上下文完全隔离,先前介绍的所有“编译期机制”(如常量表合并、const cache)在此全部失效。

在此类跨编译生命周期的场景下发挥作用的,是运行期的字符串驻留(String Interning)机制。在 Python 运行时,每当解释器生成或加载一个新的 code object,都会尝试将该代码对象内部涉及的名字和常量注册到底层的全局驻留字典中。然而,并不是所有字符串都有资格被全局驻留。

在对常量进行驻留筛选时,Python 解释器会调用底层 C 语言的 `all_name_chars` 函数进行前置检查。“本质上它检查的就是你这个 string 能不能成为一个合法的名字,也就是你这个 string 里是不是只包含了大小写字母、数字和下划线。”高天解释了这一设计的深层动机:在 Python 内部架构中,字典键查找、对象属性访问、模块符号查找等核心操作全都依赖字符串比较;如果能把所有长得像 Python 标识符的变量名/属性名在全局进行唯一化驻留,解释器在属性寻址时就能把低效的逐字符内容比较(`strcmp`)直接优化为极速的指针地址比较(`is`)。然而,系统不能无节制地缓存所有内容,否则不仅不会节省空间,反而会因哈希冲突和表膨胀导致内存飙升与性能倒退。

回到最初的代码,`"xy"` 纯由合法标识符字符构成,顺理通过了 `all_name_chars` 检查,因而在运行时被强制全局驻留;哪怕 `exec` 处于不同编译期,依然查到了已驻留的全局单例。而 `"x y"` 中间包含非法标识符字符(空格),被排除在自动驻留机制之外,因此两次独立编译分别分配了两个地址不同的独立对象。

在视频结尾,高天为不同阶段的 Python 学习者给出了核心建议:“如果你是一个 Python 初学者的话,你只需要记住一件事,就是永远不要用 is 来比较两个 string 是否相等。”因为字符串缓存机制受底层优化规则、编译作用域及解释器具体实现的深刻影响,业务逻辑中比较字符串的值应始终坚持使用 `==`。

```mermaid flowchart TD accTitle: 运行时字符串驻留检查逻辑 accDescr: 展示 Python 运行时加载 code object 时对常量字符串是否进行 intern 驻留的条件判断与流程。

Start["运行时加载 Code Object 中的常量"] --> Check{"检查 all_name_chars(str)"} Check -- "是(仅含字母、数字、下划线)\n如 'xy'" --> ValidIdent["符合合法标识符规则"] ValidIdent --> Intern["执行 String Interning\n存入全局驻留池"] Intern --> Share["跨编译单元共享同一内存指针 (is 为 True)"]

Check -- "否(含空格、特殊符号)\n如 'x y'" --> InvalidIdent["不符合标识符规则"] InvalidIdent --> NoIntern["不执行自动驻留"] NoIntern --> Separate["独立分配内存 (跨编译单元 is 为 False)"] ```

- **编译边界隔离**:`exec()` 动态执行与 REPL 单行回车均属于全新的独立编译生命周期,彻底绕过原代码文件的编译期缓存。 - **`all_name_chars` 准入检查**:只有纯由大小写字母、数字及下划线构成的合法 Python 标识符字符串,才会在运行期自动触发驻留。 - **驻留的核心价值**:服务于解释器内部快速属性查找,使高频的符号名字符串比对降维成快速的指针比较。 - **最佳工程实践**:任何场景下比较字符串的值均应严格使用 `==`,切忌依赖 `is` 判定相等性。

## Notable quotes > [07:30](https://www.youtube.com/watch?v=KGZ9gZqcCo0&t=450s) 如果你是一个 Python 初学者的话,你只需要记住一件事,就是永远不要用 is 来比较两个 string 是否相等。 — 码农高天

## Mentioned Python, 码农高天, dis, all_name_chars

## Video description 这次聊一下字符串缓存机制,这些机制如果你在看视频之前就全都知道,那你就不再需要看我视频了……

[Source](https://www.youtube.com/watch?v=KGZ9gZqcCo0)

中文简报

Python限制字符串驻留范围 仅缓存合法标识符提升寻址性能 Python仅自动驻留符合标识符命名规则的字符串以加速内部属性指针比对,由于跨编译周期及特殊字符会打破底层缓存共享,开发者判定字符串相等必须使用==而严禁使用is。 视频以一个七种字符串构造方式的对比小测验切入,深入剖析了 Python 针对不可变字符串的底层复用设计。高天通过字节码分析,逐步拆解了全切片原地直返、编译期常量表合并、常量折叠以及跨代码对象的 const cache 优化机制。随后,他进一步解释了在动态执行场景下生效的运行期字符串驻留(String Interning),揭示了其依赖合法标识符检查(all_name_chars)以加速解释器属性查找的核心逻辑。最后,视频强调业务代码中绝对不能依赖字符串缓存的偶然性,比较字符串值必须始终使用 == 而非 is。 Python 字符串作为不可变对象,其底层通过编译期常量折叠、全切片复用、const cache 与运行期字符串驻留(String Interning)等多层机制实现内存共享。 (0:00) 编译阶段会通过常量折叠将 "x" + "y" 直接合成为常量 "xy",而变量拼接(x + y)因生成 BINARY_ADD 指令必须在运行时分配新对象。 (5:00) 运行期自动字符串驻留依赖底层的 all_name_chars 检查,仅对纯由大小写字母、数字和下划线构成的合法标识符生效,带有空格的字符串在跨编译单元(如 exec 或 REPL)时不会自动复用。 (7:30) 因字符串对象复用受底层编译优化与驻留规则严格约束,业务代码中判断字符串相等性应严格使用 ==,切忌依赖 is 指针比较。 (7:30)

Reading summary

码农高天指出,Python 在底层结合了不可变对象特性、编译期常量优化与运行期字符串驻留等多重缓存机制,以此降低高频字符串带来的内存开销并提升内部查找性能。 不可变性基石与全切片就地复用 (2:30) 在针对字符串构造方式的对比测试中,直接赋值、全切片、常量拼接、函数返回乃至动态执行等方式生成的字母字符串“xy”均指向同一内存对象。高天解释,字符串属于不可变对象(immutable),其内容无法被修改,因此不同上下文可以安全共享同一份内存指针,从而节省空间。 针对切片操作,Python 解释器内建了专门的优化:当 substring 的起始点为 0 且结束点为结尾时,解释器不会像列表切片那样分配新内存,而是直接返回原字符串对象本身。同时,在同一编译单元内多次书写相同的字符串字面量,编译器只会将其在代码对象(code object)的常量表 co_consts 中记录一次,统一通过 LOAD_CONST 载入。 编译期常量折叠与跨作用域常量缓存 (5:00) 字面量拼接("x" + "y")与变量拼接(x + y)在字节码层面存在根本区别。编译器内建常量折叠(constant folding)机制,若表达式的操作数均为字面常量,编译器会在静态阶段直接求值并存入常量表;而变量拼接因运行时值不可预测,必须生成 BINARY_ADD 指令在运行时动态分配独立的新对象。 为解决跨函数或跨作用域的常量冗余问题,Python 编译器维护了 const cache 常量缓存字典。当某个 code object 编译需要新常量时,会优先检索复用此前已创建的常量实例。此外,代码对象的常量表与变量名池共用符号缓存逻辑,同名的局部变量名、函数名与字符串字面量在底层亦共享同一内存对象。 运行期字符串驻留与标识符准入检查 (7:30) 当通过 exec() 动态执行或在 REPL 交互式命令行中运行代码时,由于每次运行均触发独立的编译周期,所有编译期常量缓存均告失效。此时跨编译单元的对象共享依赖于运行期的字符串驻留(String Interning)机制,解释器会尝试将代码对象中的名字和常量注册到全局驻留字典。 为防止驻留池膨胀,解释器通过底层 C 函数 all_name_chars 设置了准入检查,仅对完全由大小写字母、数字及下划线构成的合法 Python 标识符执行自动驻留。由于 Python 的字典查找与属性访问高度依赖字符串比较,将标识符字符串全局唯一化可使逐字符比对优化为指针比较;而带有空格等非法字符的字符串(如 "x y")无法触发自动驻留,在独立编译下仍会分配不同对象。 高天总结指出,由于字符串缓存行为受编译生命周期、标识符字符构成及解释器底层规则的多重影响,开发者在实际业务逻辑中切忌使用 is 判断字符串相等性,而应始终使用 == 比较字符串内容。

Source quotes

[7:30] 码农高天: 如果你是一个 Python 初学者的话,你只需要记住一件事,就是永远不要用 is 来比较两个 string 是否相等。

Original transcript

[0:00] 这期视频我们来讲String 字符串我相信String可能是大家在python里最常使用的数据结构之一了但是你真的了解Stream吗在开始今天的视频之前我先给大家出一个小测验我们看在左边的代码里我们分别定义了7个变量这7个变量最后都是字符串xyA是直接复制的xyB是一个A翻花冒号,也就是A取一个切片C也是直接复制的xyD是字符串X加上字符串YE是x跟y这两个变量相加,这两个变量里分别保存的是字符串x和y然后我们用exec这个函数做了一个f等于xy最后我们写了一个函数这个函数returnxy我们把这个函数的返回值给复制到g里接下来我们打印这七个变量的ID我们知道在python里面两个变量如果id一致就代表他们指向的同一个对象请问你知道这七个变量里有多少变量会指向同一个对象吗正确答案是除了E也就是这个变量X+Y指向了一个不同的对象之外剩下的所有变量都指向了同一个对象让我们把刚才的代码做一些微小的修改,在X跟Y之间增加一个空格现在A到G的变量最后保存的string依然都是X空格外但在这种情况下我们程序的输出结果会和之前不同吗我们看到除了刚才的E之外由exec函数生成的f也指向了不同的对象在这简单又稍显离谱的现象背后实际上隐藏了大约四五种Python对String的缓存机制而今天我们就要把这些缓存机制一一讲明白我们知道在Python中Stream是一个immutable那对于immutable来说 由于它的数据不能被改变所以在有若干的地方需要使用同一个值的immutable的时候,我们可以让这些地方指向同一个immutable同时由于 string 是在 python 里面非常非常长的数据结构通过这种缓存机制我们可以节省大量的内存一个最容易理解但是放在这稍显不恰当的例子就是左边这个例子当我们建立了一个string x to y 把它复制给 A 的时候如果我们想把a里面这个值再复值给b对于immutable来说,我们当然可以把这个string整个复制一份扔给B但是我们也可以让A跟B指向同一个object由于这个string没有办法被改变所以这两件事是等价的当然了这个例子在这里不是非常合适因为在python里面这个assignment就代表让这两个变量指向同一个对象那就让我们把这个例子做的稍微复杂一点现在我用一个A slice这里我们用到了一个取切片的操作在刚才的例子里面我们可以知道取完这个切片之后A跟B竟然指向的还是同一个对象而不是一个复制过一遍的stringxy而这件事的原因很简单这就是我们要讲的第一个机制当你做substrain这个操作的时候如果你的起始点是0结束点是最后它就会直接返回本身这个没有改变过的Stream因此在看似复制的切片操作背后 它实际上没有复制在这里string和list的行为是不一样的当你在做列表的切片操作的时候它是做复制的接下来我们看把同一个string xy分别复制给a和b那在这里你就要稍微了解一下python的编译机制大家可以回头看我之前的视频因为如果你不了解那个机制的话你就不知道python在这里到底是怎么运行的你可能就会觉得我写了两个xy 他俩就分别代表两个不同的string事实上用this module我们就可以看到他在编译的过程中建立了一个code object然后他发现这个string xy应该是我需要使用到的一个长量他就把这个string xy作为一个长量保存到了它的co-const里我们可以看到在我们做storenameA跟storenameB之前我们使用的是 [5:00] 同一个命令loadConst0也就是说在编译期的时候Python就知道了这个A跟B指向的根本就是同一个常量有的人可能说好这个我可以理解那为什么A等于XY和B等于X加Y竟然指向的还是同一个object呢看一下这段Python代码对应的字节码你就明白了它对应的字节码跟刚才那个程序是一模一样的这是Python在编译期做的一个优化它把能计算的常量先提前计算出来了它算出来了你这个X加Y就是XY于是把字节码优化成了跟之前一样的结果那理所当然的现在的A跟B指向的也是同一个object与之相比如果我们把X跟Y这两个string保存在X跟Y这两个变量里然后B是XY这两个变量相加的话在字解码中我们就能看到这里明确的做了一个binary add也就是在运行时做了一个字符串相加的操作那它生成的这个object必然就和我们保存的这个constant object不一样了接下来这个问题看起来就更神奇了我们定义了一个函数g让它返回xy同时我们把xy这个constant直接复制给a这个时候a跟b竟然还是指向的同一个object这又是为什么呢这是因为python在编译期的时候建立了一个叫做const cache的东西这个const cache就专门缓存各种常量也就是说在python编译代码的时候每当他需要用一个新的常量的时候他就会先回头找一找之前是不是也用到了同样的常量如果之前用到了它就会让这个新的常量直接指向之前已经开开的那个常量这样就可以更省内存了就不用在建立每一个code object 时候都为它单独建立只属于它的常量 object事实上我们可以看到在建立code object 的时候它的常量和变量名实际上是采用的同一个换存机制所以如果我们很无聊比如说在函数 g 里面建立一个局部变量名字叫 xy然后我们再建立一个叫 xy 的函数接着我们把 xy 这个 string 复制给 a当我们去查看 g 这个函数里保存 xy 这个名字的变量保存xy这个函数名的变量和保存xy这个string的变量a的时候你会发现它们指向的是同一个object好那最后就是看起来最荒谬的一个部分了同样是负值和exec的结果做比较我们会发现如果这个string是xy的时候a和b的id就是一样的但如果这个string变成了x空格外结果就变了事实上你可以在python的repl里面更简单的复现这个问题当你在交互式界面打a等于x空格外b等于x空格外a is b的时候会显示这个是false但你只要把x空格外变成xy a is b就是true这背后的机制我们叫做string interning我们之前讲的所有机制都是在编译期完成的而我们知道使用exec函数是要重新编译这段代码的所以在运行这段代码的时候a跟b c跟d都不是在同一个编译期被编译出来的也就是所有编译期的优化对它们都是没有用的当然在交互式界面中每一次回车也是一个全新的编译所以在交互式界面里面哪怕你不使用exec也可以复现这个效果那在没有了这些编译期的机制之后为什么当a等于xy的时候a跟b还能指向同一个string呢因为在python的运行期也就是runtime每次我们尝试建立一个新的code object的时候他都会尝试把这个code object里面的名字和长量做一个intern这个intern你可以就理解成为缓存也就是说在编译期之外在运行期 python 依然有一层string的缓存机制而注意在这个缓存中 [10:00] 机制力对于所有的Constant他首先要检查一下all name charts这个 all name charts 函数也很简单本质上它检查的就是你这个 string 能不能成为一个合法的名字也就是你这个string里是不是只包含了大小写字母数字和下方线那做这个判断的原因应该是在python里面所有的变量名属性名都是以stream的形式保存在一个地方的所以当长量长得像一个变量名的时候Python就认为这个东西值得一存因为你不能缓存每一个东西嘛你要是所有东西都缓存的话会拖慢你程序的运行速度也会反而增加你内存的消耗回到我们开始的问题,我们显然已经知道答案了。尽管我们的A等于XY和EXECB等于XY是在两个编译期运行的但是在run time的时候 由于xy这个长量是可以成为一个合法的变量名的所以Python的运行时依然把它换存了相反的,x空格外这个常量并不符合python的标准所以Python就没有缓存它于是C跟D就指向了不同的object总结一下今天的内容如果你是一个python初学者的话你只需要记住一件事就是永远不要用is来比较两个string是否相等如果你是python的重度使用者或者很喜欢研究python的机制的话,你只需要额外再多记住一件事就是关注我收获更多别人那听不到的Python知识好 这期视频在这里 喜欢的话 记得点下关注 别忘了一键三流哦字幕志愿者 杨茜茜