码农高天
全局字典持续持有强引用会导致垃圾回收失效并引发内存膨胀,采用弱引用容器可在外部引用归零时自动剔除键值对,但使用__slots__时必须显式声明__weakref__以防程序崩溃。
AI-assisted summary of 【python】weakref是什么?带你入门弱引用!看了才知道程序可以这么写 — original source. Check the original for full context.
Source published:
Key ideas
- 使用全局普通字典缓存对象时,由于强引用导致对象的引用计数无法归零,即使局部变量离开作用域也会引发严重的内存泄漏。 (0:00)
- weakref 模块提供了 WeakValueDictionary 与 WeakKeyDictionary,能够在外部强引用归零时由垃圾回收自动清理对应的字典键值对,无需手动调用 pop 或清空字典。 (2:30)
- Python 的大多数底层内置类型(如 int、str、list 等)不支持弱引用,且定义了 __slots__ 的自定义类必须显式包含 '__weakref__' 才能正常使用弱引用机制。 (5:00)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2024 年 7 月 12 日发布。视频简介指出,许多开发者对弱引用机制缺乏了解,甚至在面对契合的业务需求时也不知道能用其解决,本期视频旨在帮助大家建立对弱引用的基本认知与应用意识。
## TL;DR 视频通过一个游戏玩家全局缓存字典导致内存泄漏的直观案例,剖析了强引用阻碍垃圾回收(GC)的技术痛点,并引出弱引用的核心概念:当对象不再拥有外部强引用时,弱引用容器会自动释放该对象。作者重点演示了标准库 `weakref` 中 `WeakValueDictionary` 与 `WeakKeyDictionary` 的实际用法与自动清理机制,能够平滑替换常规字典。最后,高天深入到底层机制,解释了为何 `int`、`str` 等内置类型不支持弱引用,并提醒开发者若使用 `__slots__` 优化内存,必须显式声明 `'__weakref__'` 以免运行时报错。
## Atmosphere & style 视频整体呈现出一种务实、干练且极具技术说服力的“老码农式”教学风格。高天从一段人人都能看懂的泄漏代码切入,随着代码修改与输出结果的即时验证,节奏紧凑明快,直奔痛点。讲解语调从容松弛又直击要害,偶尔流露出“手动 pop 稍微有点蠢”的幽默自嘲,让硬核的底层引用与 GC 机制变得生动易懂、豁然开朗。
## Bulletin - [00:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=0s) 使用全局普通字典缓存对象时,由于强引用导致对象的引用计数无法归零,即使局部变量离开作用域也会引发严重的内存泄漏。 - [02:30](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=150s) weakref 模块提供了 WeakValueDictionary 与 WeakKeyDictionary,能够在外部强引用归零时由垃圾回收自动清理对应的字典键值对,无需手动调用 pop 或清空字典。 - [05:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=300s) Python 的大多数底层内置类型(如 int、str、list 等)不支持弱引用,且定义了 __slots__ 的自定义类必须显式包含 '__weakref__' 才能正常使用弱引用机制。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=0s) 在类的初始化中将实例放入全局普通字典会导致引用计数不归零,使生命周期已结束的局部对象无法被 GC 回收。 - [00:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=0s) 手动使用 del/pop 移除缓存或在业务结束后暴力清空字典扩展性差且极其脆弱,无法适应复杂系统的维护需求。 - [00:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=0s) 弱引用的核心定义是对对象的引用不构成其存活的充分条件,外部其他引用全部归零后该对象会被正常释放。 - [02:30](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=150s) WeakValueDictionary 对 value 保持弱引用,非常适合按唯一 ID 建立全局缓存并在对象无外部强引用时自动移除。 - [02:30](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=150s) WeakKeyDictionary 对 key 保持弱引用,适合以对象实例作为键来附加外部元数据,且不会干涉对象的生命周期。 - [02:30](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=150s) 弱引用字典保留了常规字典的读写操作习惯,平替普通字典时无需对已有业务逻辑进行大规模重构。 - [02:30](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=150s) 调试弱引用字典时,可直接将其转换为普通 dict 进行打印,以便直观查看当前仍然存活的键值对状态。 - [05:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=300s) int、str、list、tuple 等大部分 C 语言实现的内置基础类型原生不支持弱引用,尝试对其创建弱引用会直接报错。 - [05:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=300s) Python 层面自定义的绝大多数 class 实例、方法、集合和生成器等默认均支持弱引用。 - [05:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=300s) 使用 __slots__ 优化类内存时会默认移除弱引用能力,必须在 __slots__ 元组中显式声明 '__weakref__' 才能规避 TypeError 异常。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=0s) 全局缓存引发的内存泄漏与弱引用概念
码农高天首先给出了一个直观的示例代码:程序中定义了一个非常简单的 `Player` 类,在它的 `__init__` 初始化函数中,为每个玩家赋予一个唯一的 `id`,并将新建的玩家对象直接存入一个全局字典 `players` 中。在实际业务中,这种全局字典或缓存字典非常普遍,通常用来记录哪些 ID 已经被占用,或是充当缓存以便后续随时查找特定的玩家。随后,程序定义了一个 `game()` 函数来模拟单场游戏。在每一场游戏内,代码会实例化两个玩家 `p1` 和 `p2`,后续可能会执行一些具体的游戏逻辑。在单场游戏运行结束之后,局部变量 `p1` 和 `p2` 的生命周期理应终止;当下一场新游戏开始时,系统创建的又是两个崭新的玩家实例。
```mermaid flowchart TD accTitle: 全局字典强引用导致的内存泄漏与弱引用对比 accDescr: 对比强引用字典与弱引用字典在局部变量离开作用域后的不同行为。
subgraph NormalDict["方案 A:普通强引用字典(内存泄漏)"] G1["局部变量 p1, p2 (作用域结束销毁)"] -.->|"离开函数时失效"| Obj1["Player 对象"] D1["全局 players 字典"] -->|"持有强引用 (强保持)"| Obj1 Obj1 -->|"引用计数无法归零"| Leak["无法被 GC 回收(字典持续膨胀)"] end
subgraph WeakDict["方案 B:WeakValueDictionary(自动清理)"] G2["局部变量 p1, p2 (作用域结束销毁)"] -.->|"离开函数时失效"| Obj2["Player 对象"] D2["WeakValueDictionary"] -.->|"持有弱引用 (不计入生命周期)"| Obj2 Obj2 -->|"外部无强引用时"| GC["GC 释放对象并自动移除键值对"] end ```
然而,如果连续运行多场游戏,就会暴露出严重的内存隐患。因为代码一直在向 `players` 字典不断填充数据,却从未把用完的玩家对象拿出来。尽管开发者心知肚明每次 `game()` 调用结束后 `p1` 和 `p2` 已经毫无用处,但由于 Python 的垃圾回收(GC)主要依赖引用计数机制,虽然局部变量名失效了,全局字典却依然牢牢保持着对这些对象的强引用。随着时间推移和游戏持续进行,字典最终会“大到爆炸”。针对这种情况,硬编码去手动释放并不理想:在函数末尾手动用 `del` 或 `pop` 移出对象显得极其不优雅,“稍微有点蠢”;而在每次游戏结束后直接清空字典,在真实复杂的系统中更是完全无法扩展(scale)。开发者真正期待的理想行为是:一旦该对象在外界已经没有任何强引用了,字典就应该自动放弃它。针对这种“当全世界只有你这个字典里还保存着这个 player 的时候,你就不需要保存它了”的需求,Python 提供了专门的机制——弱引用(Weak Reference)。弱引用的核心在于:我对你的引用不是强引用,并不构成你存活的充分条件,一旦外部其他引用全部归零,对象就会被正常释放。
- 全局字典引用计数不归零会导致经典的对象泄漏,即使局部变量脱离作用域也无法被 GC 回收。 - 手动从字典弹出(pop)或定时清空字典的方案扩展性极差,代码一旦复杂极易遗漏报错。 - 弱引用机制由 Python 内置的 `weakref` 模块提供支持。 - 正如码农高天所强调的:“弱引用顾名思义就是我对你的引用啊不是一个强引用,也就是说如果你这个 object 身上其他的引用都没有了,你就可以被 release 了,我对你的引用并不构成你存活的条件。”
[02:30](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=150s) WeakValueDictionary 与 WeakKeyDictionary 的实战用法
在 `weakref` 模块中,最基础且最常用的两大容器是 `WeakValueDictionary` 和 `WeakKeyDictionary`。码农高天重点演示了 `WeakValueDictionary` 的替换流程。从使用习惯上看,它本身就是一个标准的字典结构,常规的字典读写操作完全不变,代码无需大规模重构;其特殊之处在于它只对字典中存储的 `value`(值)保持弱引用。一旦某个 `value` 对象的外部强引用归零,该对象被垃圾回收的同时,`WeakValueDictionary` 会自动将该对象及其对应的键(`key`)彻底从字典中剔除。在代码演示中,仅仅将全局变量 `players` 替换为 `weakref.WeakValueDictionary()`,在 `game()` 函数执行期间打印字典,可以看到玩家对象尚存;而一旦跳出 `game()` 函数的作用域,由于 `p1` 和 `p2` 局部强引用消失,再次打印字典时 `players` 已经自动被清空。
与此对应的另一个数据结构是 `WeakKeyDictionary`。它的工作机制是字典对存储的 `key`(键)保持弱引用,只要外界对作为键的对象失去了强引用,对应的整个键值对就会自动从字典中释放。为了验证这一点,高天将初始化代码修改为反向存储,即把玩家实例自身作为字典的键,玩家的 ID 作为值(`players[self] = self.id`)。在替换为 `WeakKeyDictionary` 之后,程序再次运行并在 `game()` 函数结束后检查,字典同样能够如预期般自动清空,展现了弱引用在不同映射方向下的灵活控制能力。
- `WeakValueDictionary`:对键值对中的 Value 进行弱引用,适合全局对象缓存、按 ID 检索实例等场景。 - `WeakKeyDictionary`:对键值对中的 Key 进行弱引用,适合为现有对象附加外部元数据或属性而又不干涉其生命周期。 - 打印调试注意点:直接打印弱引用字典时,可先转换为普通字典(如 `dict(players)`)以获得更直观的输出视图。 - 码农高天总结其内部触发机制时解释道:“因为在 game 函数结束之后 P1 和 P2 这两个引用都没有了,所以在 players 这个字典里面,这两个值和他们对应的 key 就都被 release 了。”
[05:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=300s) 不支持弱引用的对象类型与 `__slots__` 避坑指南
深入到底层机制时,高天指出并不是 Python 中的所有对象都支持弱引用。如果在未修改键值对对应关系的情况下,直接将普通的 `WeakKeyDictionary` 作用于整数 ID 作为键的字典,程序在运行时就会立即崩溃报错,提示不能对一个 `int` 对象创建弱引用;即便把键换成字符串 `str`,结果依然报错。这是因为在 Python 中,有相当一部分基于 C 语言实现的内置对象(built-in objects,如 `int`、`str`、`list`、`tuple` 等)底层都不支持弱引用。然而这在绝大多数开发场景下并没有任何影响,因为对基础类型做弱引用本身就毫无意义——小整数等基础对象在解释器内部是永久驻留存活的,大整数或字符串也极易被无关代码共用,对其生命周期做弱引用监控既不现实也无必要。
```mermaid flowchart TD accTitle: Python 对象弱引用支持分类 accDescr: 展示哪些 Python 对象默认支持弱引用,哪些需要配置 __slots__,哪些完全不支持。
Types{"Python 对象类型"}
Types -->|"原生支持弱引用"| Supported["大部分 Python 层自定义 Class<br/>实例方法 (Methods)<br/>集合 (Sets)、生成器 (Generators)"] Types -->|"完全不支持弱引用"| Builtin["大部分 C 扩展内置对象<br/>(如 int, str, list, tuple 等)"] Types -->|"条件支持 (易错坑点)"| SlotsClass["定义了 __slots__ 的 Class"]
SlotsClass -->|"未显式包含 '__weakref__'"| SlotFail["抛出 TypeError (不支持弱引用)"] SlotsClass -->|"__slots__ 中声明了 '__weakref__'"| SlotPass["分配弱引用指针槽位 (正常支持)"] ```
在实际开发中,在 Python 层面自定义的绝大多数 class 实例、方法、集合或生成器等,默认都原生支持弱引用。但这里存在一个极易踩坑的经典隐患:如果自定义类中定义了 `__slots__`。为了节省内存开销,有些开发者会使用 `__slots__` 来限制属性,但一旦声明了 `__slots__`,该类创建的对象默认将无法被弱引用。如果要在使用了 `__slots__` 的类中继续使用弱引用,开发者必须在 `__slots__` 元组中显式补上 `'__weakref__'` 这一项。高天提醒,不使用 `__slots__` 固然省心,但如果为了性能优化开启了 `__slots__`,就千万不能漏掉这个用来存放弱引用相关内部信息的属性槽位。
- 许多 C-level 内置对象(如 `int`、`str` 等)由于内部结构没有弱引用指针头,不支持弱引用。 - Python 层面定义的类实例、生成器对象、集合等默认均支持弱引用。 - 关键避坑:定义了 `__slots__` 的类如果需要支持弱引用,必须在 `__slots__` 中加入 `'__weakref__'`,否则尝试创建弱引用时会引发异常。 - 码农高天在结尾叮嘱大家注意这个细节:“不定义 slots 没事,如果定义了 slots,记得在 slots 里面加上它。”
## Notable quotes > [00:00](https://www.youtube.com/watch?v=XPGH0sBfPn0&t=0s) 弱引用顾名思义就是我对你的引用啊不是一个强引用,也就是说如果你这个 object 身上其他的引用都没有了,你就可以被 release 了,我对你的引用并不构成你存活的条件。 — 码农高天
## Mentioned 码农高天, Python, weakref, WeakValueDictionary, WeakKeyDictionary, __slots__, __weakref__, C
## Video description 这期视频聊一下弱引用,其实很多人对弱引用是完全不了解的,甚至有了需求都不知道可以用弱引用来解决问题。看完这期视频希望大家可以知道有这么个东西。
[Source](https://www.youtube.com/watch?v=XPGH0sBfPn0)