CPython利用__slots__剔除实例字典 属性访问提速10%

【python】__slots__是什么东西?什么?它还能提升性能?它是如何做到的!? — 码农高天

码农高天

__slots__机制在类创建期将属性映射为C结构体的固定内存偏移量,借由描述器协议实现直接指针寻址,在彻底消除实例字典内存开销的同时省去哈希查找步骤。

AI-assisted summary of 【python】__slots__是什么东西?什么?它还能提升性能?它是如何做到的!? — original source. Check the original for full context.

Source published:

Key ideas

  • 在 Python 类定义中使用 __slots__ 可以构建属性白名单,不仅能防止动态绑定意外属性,还能在基准测试中让属性赋值速度提升约 10%。 (0:00)
  • CPython 在类创建阶段(type_new)会为声明的每个 slot 计算相对于对象基地址的固定内存偏移量(offset),并将槽指针连续追加在 C 结构体末尾。 (2:37)
  • 在 PyType_Ready 阶段,CPython 会在类字典中为每个 slot 生成 Member Descriptor,使得属性访问直接转化为基地址加偏移量的快速指针寻址,完全省去实例哈希表的检索开销。 (6:50)
  • 使用 __slots__ 的类默认不再为实例创建 __dict__ 字典,彻底消除了动态哈希表的内存预留与冗余开销,实现“一个萝卜一个坑”的内存紧凑布局。 (9:56)

Detailed notes

## Background 该视频由技术博主“码农高天”于 2024 年 2 月 12 日发布在 YouTube 平台上。视频简介提到,__slots__ 在国内技术社区(如 B 站)讲解较少,但它不仅易于使用,还能显著提升程序的性能和稳定性,本期视频旨在深入剖析其背后的底层运行原理。

## TL;DR 视频全面解析了 Python 中 __slots__ 的核心机制与三大优势:限制属性种类的白名单、加快约 10% 的属性访问速度以及大幅压缩内存。博主结合 CPython 源码,层层拆解了类创建阶段 type_new 中内存偏移量(offset)的计算与分配,以及 PyType_Ready 阶段如何生成并注册 Member Descriptor。通过原理对比与代码实测,视频清晰论证了 __slots__ 为何能省去普通实例维护 __dict__ 哈希表的巨大内存开销,并凭借“基地址 + 偏移量”的直接指针运算实现高效寻址。

## Atmosphere & style 视频呈现出严谨扎实且极具探索欲的技术极客风格。码农高天以温和从容、抽丝剥茧的语调,带领观众直接下潜至 C 语言层面的 CPython 源码世界,将原本抽象隐晦的底层内存布局拆解得一清二楚。生动的“一个萝卜一个坑”比喻与严谨的基准测试、交互式终端验证相互映衬,既有硬核源码调试的张力,又有解开底层黑盒后的豁然开朗与自信从容。

## Bulletin - [00:00](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=0s) 在 Python 类定义中使用 __slots__ 可以构建属性白名单,不仅能防止动态绑定意外属性,还能在基准测试中让属性赋值速度提升约 10%。 - [02:37](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=157s) CPython 在类创建阶段(type_new)会为声明的每个 slot 计算相对于对象基地址的固定内存偏移量(offset),并将槽指针连续追加在 C 结构体末尾。 - [06:50](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=410s) 在 PyType_Ready 阶段,CPython 会在类字典中为每个 slot 生成 Member Descriptor,使得属性访问直接转化为基地址加偏移量的快速指针寻址,完全省去实例哈希表的检索开销。 - [09:56](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=596s) 使用 __slots__ 的类默认不再为实例创建 __dict__ 字典,彻底消除了动态哈希表的内存预留与冗余开销,实现“一个萝卜一个坑”的内存紧凑布局。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=0s) __slots__ 的三大核心优势是限制属性种类(白名单提高鲁棒性)、加快属性访问速度以及大幅压缩内存占用。 - [00:00](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=0s) 向带有 __slots__ 的类实例赋予白名单之外的属性时,Python 会直接抛出 AttributeError 异常。 - [00:00](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=0s) 基准测试表明,使用 __slots__ 相比普通类在循环属性赋值操作中能获得大约 10% 的运行速度提升。 - [02:37](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=157s) __slots__ 的底层实现核心发生在 CPython 的 type_new 函数中,负责解析 slot 声明并改造类的内存布局。 - [02:37](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=157s) CPython 解析 __slots__ 时会统一将其转换为元组,滤除 __dict__ 与 __weakref__ 等特殊名字后保存到 ht_slots。 - [02:37](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=157s) Python 对象在底层本质是 C 结构体,声明 slots 时会在原本对象的 tp_basicsize 基础上连续追加固定大小的内存空间。 - [02:37](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=157s) 每个 slot 在底层都会构建一个 member 结构体,其类型标记为 T_OBJECT_EX,偏移量则按 sizeof(PyObject*) 递增累加。 - [06:50](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=410s) 类就绪阶段的 PyType_Ready 函数通过 add_members 为每个 slot 创建 Member Descriptor 并注册到类的 __dict__ 中。 - [06:50](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=410s) 根据 Python 描述器协议,访问实例属性时会优先命中类级描述器,从而跳过对实例字典的检索。 - [06:50](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=410s) 底层读取函数 PyMember_GetOne 通过实例基地址加上固定偏移量直接定位内存,避免了运行时的动态哈希查找。 - [09:56](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=596s) 普通 Python 对象的属性读取需要先查类 MRO 描述器,未命中后再查实例 __dict__,多出了一次哈希表查询开销。 - [09:56](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=596s) 字典为了保证 O(1) 性能需保留大量冗余空间,而 __slots__ 采用“一个萝卜一个坑”的紧凑结构,极大节省了内存。 - [09:56](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=596s) 定义了 __slots__ 的实例默认完全不创建 __dict__ 属性,直接访问实例的 __dict__ 会抛出异常。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=0s) \_\_slots\_\_ 的基本概念与三大核心优势

在 Python 编程中,`__slots__` 是一个可以在类定义(class definition)中声明的特殊变量,其类型通常是一个元组(tuple)或者列表(list)。它的最直观作用是限制该类的实例对象能够使用的属性种类。码农高天通过一个直观的代码示例进行了演示:定义一个类 `A` 并在其中声明 `__slots__ = ('f', 'g')`,随后实例化一个对象 `o = A()`。当尝试执行常规的动态属性赋值 `o.b = 2` 时,程序会立刻抛出异常,提示 `AttributeError: 'A' object has no attribute 'b'`;而如果将赋值语句改为 `o.f = 2`,代码就能顺利执行。这是因为开发者通过 `__slots__` 显式设置了一份属性白名单,明确告知 Python 该类的实例仅允许绑定名为 `f` 和 `g` 的属性。

然而,白名单机制带来的代码规范性只是 `__slots__` 的冰山一角。码农高天随后展示了一个对比基准测试(benchmark):定义包含 slots 的类 `A` 与完全不包含 slots 的普通类 `B`,分别生成其实例 `o` 和 `p`,并在高频循环中执行相同次数的属性赋值操作(`o.f = 2` 与 `p.f = 2`)。基准测试结果表明,带有 `__slots__` 的类在属性赋值性能上显著优于普通类,速度大约提升了 10%。不仅如此,在内存占用方面它也展现出了惊人的压缩效果。

码农高天总结道:“所以说 slots 这个机制实际上至少给我们带来了三件事:第一,通过限制属性的种类,利用白名单这个制度让我们的代码更有鲁棒性;第二,在 access 这些属性的时候速度更快;第三,实际上它是更省内存的。”

- **属性白名单**:通过元组或列表形式限制类实例可绑定的属性名,防止动态添加预期之外的属性。 - **性能提升**:基准测试中,属性赋值与访问速度通常能获得约 10% 的性能优化。 - **内存优化**:消除了每个实例默认附带的字典结构,大幅降低大规模对象实例化时的内存占用。

---

[02:37](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=157s) CPython 源码剖析:类创建阶段的 type_new 与 Member 内存偏移

为了探究底层性能提升的根本原因,必须深入 CPython 的源码实现。码农高天指出,`__slots__` 绝大多数底层的“魔法”都在类型创建阶段发生,其核心逻辑位于 CPython 源码的 `type_new` 函数(即创建新 class 或新 type 时触发的函数)中。当解析到类的定义包含 `__slots__` 时,CPython 首先将属性值存入局部变量,统一转换为元组格式,并做基本检查和特殊处理——如滤除 `__dict__` 与 `__weakref__` 这两个特殊 slot 并保留普通 slot 名字,对其排序后再保存到堆类型对象的 `ht_slots` 中。

接下来便进入了最关键的内存布局改造阶段。在底层实现中,所有的 Python 对象(包括类对象本身)在 C 语言层面本质上都是一个 C 结构体(C Struct)。CPython 通过宏 `PyHeapType_GET_MEMBERS` 获取该类型结构体尾部的指针。普通对象的类型具有一个基准内存大小(`tp_basicsize`),而每当类定义中声明了一个额外的 slot,Python 在计算并申请内存时就会在原本的 C 结构体末尾额外追加一段连续空间。宏最终返回的指针正是指向这片追加区域,用于保存该类型的成员元数据(member 信息)。

对于每一个 slot,底层都会建立一个 `member` 结构体,将其 `name`、`type` 与 `offset` 逐一赋值。其中,`type` 统一标记为 `T_OBJECT_EX`,表明槽内存储的是一个 `PyObject` 指针;而 `offset`(偏移量)则初始设定为对象的基准大小 `tp_basicsize`。随后,每遍历一个 slot,`slot_offset` 就会向后累加一个 `sizeof(PyObject*)` 的字节长度。码农高天举例解释道:假设 `class A` 正常情况下需要 100 个字节来保存,其初始 `slot_offset` 就是 100;第一个 slot `f` 的偏移量就固定在 100,紧随其后的第二个 slot `g` 则位于 108。这就相当于为每个新创建的实例划分好了专属的连续内存空间,最后将包含这些偏移量信息的结构体数组指针保存到类型的 `tp_members` 中。

```mermaid flowchart TD accTitle: CPython 类创建阶段 slots 内存布局处理流程 accDescr: 展示从读取 slots 到计算内存偏移量并存入 tp_members 的完整步骤

subgraph Phase1["1. 解析 slots 定义"] A["读取 __slots__ 声明"] --> B["转换为 Tuple 并过滤 __dict__ / __weakref__"] B --> C["对 slots 属性名排序并存入 ht_slots"] end

subgraph Phase2["2. 计算连续内存偏移量"] D["基准大小 tp_basicsize (如 100 bytes)"] --> E["Slot 'f': offset = 100"] E -->|"累加 sizeof(PyObject*)"| F["Slot 'g': offset = 108"] end

subgraph Phase3["3. 构建 Member 结构体"] G["构建 member 结构体 (Type: T_OBJECT_EX)"] H["记录各属性名称与绝对偏移量"] I["保存结构体数组至类的 tp_members"] end

Phase1 --> Phase2 Phase2 --> G G --> H H --> I ```

- **C 结构体内存扩展**:对象在内存中是定长的 C 结构体,每个 slot 都会直接在其后方顺延开辟一个指针长度的固定存储单元。 - **偏移量计算机制**:首个 slot 的偏移值直接对齐类型的 `tp_basicsize`,后续 slot 依序递增 `sizeof(PyObject*)`(64 位系统下通常递增 8 字节)。 - **`T_OBJECT_EX`**:标识 slot 内存储的值是一个 Python 对象指针。

---

[06:50](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=410s) PyType_Ready 与 Member Descriptor:属性读写的底层运作

在 `type_new` 函数中完成属性名与偏移量(offset)的记录后,类本身的数据结构已经完备,但实例运行时(runtime)的读写行为还没有改变。底层的后续动作发生在类初始化的收尾函数 `PyType_Ready` 中。在类型就绪的过程中,CPython 会调用 `add_members` 函数。该函数遍历先前保存在 `tp_members` 中的所有成员元数据,并执行两个核心操作:首先为每一个 member 创建一个专门的“描述器”(Member Descriptor),随后将这个描述器直接注册到类的字典(class `__dict__`)中。

当我们在 Python 代码中访问实例属性(例如调用 `o.f`)时,Python 的属性解析顺序(MRO)规定了必须首先检查该对象所属类的 MRO 列表中是否存在同名描述器。由于 `add_members` 已经将 `f` 的 Member Descriptor 预先放入了 `A.__dict__` 中,解释器便会优先命中该描述器,直接调用描述器绑定的获取方法(getter)和设置方法(setter)。

描述器的核心读取逻辑由底层 C 函数 `PyMember_GetOne` 实现。该函数接收实例对象 `o` 的绝对内存地址以及当初记录的成员结构体 `DMember`。函数内部通过将实例对象的内存基地址直接加上此前计算好的固定偏移量(如基地址 `ADDR + 100`),即可毫无阻碍地定位到 slot 所指的那片内存,将其强制转换为 `PyObject*` 指针并返回给上层代码。

码农高天强调道:“在这里把这个 address 加了一个 offset……这个 ADDR 呢保存的是这个 object 本身的绝对地址,绝对地址加上 offset 就是我们要找的这个 slot 里面保存的这个 object 地址。”这一设计彻底避开了运行时的动态解析,使得属性访问转化为了纯粹的高效指针偏移运算。

- **Member Descriptor 生成**:在 `PyType_Ready` 阶段,类字典中自动填充对应 slot 的描述器对象。 - **属性查找优先级**:基于 Python 描述器协议,实例查找属性时首先命中类层级的描述器,跳过了对普通实例字典的查询。 - **指针寻址(Pointer Arithmetic)**:底层读取直接执行 `(PyObject **)((char *)addr + offset)`,耗时极低。

---

[09:56](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=596s) 为什么更快更省内存:原理解析与代码验证

通过对底层机制的全景还原,`__slots__` 能够提升运行速度并节省内存的本质原因变得十分清晰。在没有使用 `__slots__` 的普通 Python 对象中,查找属性的操作必须经历两步:第一步,依然需要遍历类及其父类的 MRO 字典以确认是否存在描述器(这一步是不可避免的开销);第二步,在未找到对应描述器的情况下,解释器被迫再去该实例对象专有的 `__dict__` 字典中执行一次哈希查找。而使用了 `__slots__` 之后,属性查找在第一步检查类级描述器时就已精准命中目标,描述器凭借内置的固定偏移量直接把变量的绝对内存地址反馈给解释器,完全省去了第二阶段访问实例哈希表的开销。

在内存分配方面,差距更为悬殊。码农高天用生动的比喻指出:“如果你用 slots 的话,在每一个 object 上都是一个萝卜一个坑;如果你不用 slots 的话,你需要用一个 dictionary 来保存这些萝卜。”字典在底层是一种动态扩容的哈希表结构,为了维持平均 $O(1)$ 的查找性能,哈希表内部必须预留可观的冗余空间(低装载因子),这意味着存储普通字典所需的内存远高于所存数据本身的体积。更激进的是,使用 `__slots__` 的类创建其实例时,默认甚至压根不会为实例创建 `__dict__` 属性,实例结构体仅包含基础对象头和紧挨着的预留槽指针。

在视频的最后,码农高天通过实际交互代码对上述理论进行了验证: 1. 打印 `A.__dict__` 与 `B.__dict__`:带有 slots 的类 `A` 的字典中赫然存在 `f` 与 `g` 两个特殊的 `member_descriptor`,而普通类 `B` 中则没有。 2. 打印实例自身的 `__dict__`:普通实例 `p` 的 `p.__dict__` 输出了 `{'f': 2}` 键值对;而使用了 slots 的实例 `o` 根本没有 `__dict__` 属性,直接访问 `o.__dict__` 会报错,证明其属性值确实没有落入动态字典,而是安全保存在预先分配的连续空间中。

码农高天在结尾充满信心地总结道:“这可能是整个 B 站讲的最详细的 slots 教程了,希望对大家有所帮助。”

| 对比维度 | 普通类(未定义 `__slots__`) | 使用 `__slots__` 的类 | |:--- |:--- |:--- | | **属性白名单限制** | 无限制,可动态追加任意属性 | 严格受限于定义的 slots 元组/列表 | | **实例属性存储载体** | 实例私有的动态哈希表 `__dict__` | 结构体尾部连续的直接指针空间(“一个萝卜一个坑”) | | **实例 `__dict__` 存在性** | 默认必然存在并占用空间 | 默认完全不创建,零字典开销 | | **属性查找流程** | 1. 查找 MRO 描述器(未命中)<br>2. 检索实例自身 `__dict__` 哈希表 | 1. 查找 MRO 描述器(命中 Member Descriptor)<br>2. 基于基址+偏移量直接寻址返回 | | **性能与内存表现** | 存在哈希开销、冗余内存分配,速度相对较慢 | 速度快约 10%,显著压缩内存占用 |

## Notable quotes > [09:56](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=596s) 如果你用 slots 的话,在每一个 object 上都是一个萝卜一个坑;如果你不用 slots 的话,你需要用一个 dictionary 来保存这些萝卜。 — 码农高天

> [00:00](https://www.youtube.com/watch?v=FqBKqYzaRaQ&t=0s) slots 这个机制实际上至少给我们带来了三件事:第一,通过限制属性的种类,利用白名单这个制度让我们的代码更有鲁棒性;第二,在 access 这些属性的时候速度更快;第三,实际上它是更省内存的。 — 码农高天

## Mentioned Python, CPython, 码农高天, B站, type_new, PyType_Ready, add_members, PyMember_GetOne, Member Descriptor, PyHeapType_GET_MEMBERS, PyObject

## Video description 这期视频我们来讲一下__slots__。这个内容在B站上讲的人很少,但是还蛮有趣的,因为它是可以提升我们的程序性能和稳定性的,而且并不难使用。我们的视频当然和往常一样,主要是要介绍它背后的原理啦!

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

中文简报

CPython利用__slots__剔除实例字典 属性访问提速10% __slots__机制在类创建期将属性映射为C结构体的固定内存偏移量,借由描述器协议实现直接指针寻址,在彻底消除实例字典内存开销的同时省去哈希查找步骤。 视频全面解析了 Python 中 __slots__ 的核心机制与三大优势:限制属性种类的白名单、加快约 10% 的属性访问速度以及大幅压缩内存。博主结合 CPython 源码,层层拆解了类创建阶段 type_new 中内存偏移量(offset)的计算与分配,以及 PyType_Ready 阶段如何生成并注册 Member Descriptor。通过原理对比与代码实测,视频清晰论证了 __slots__ 为何能省去普通实例维护 __dict__ 哈希表的巨大内存开销,并凭借“基地址 + 偏移量”的直接指针运算实现高效寻址。 在 Python 类定义中使用 __slots__ 可以构建属性白名单,不仅能防止动态绑定意外属性,还能在基准测试中让属性赋值速度提升约 10%。 (0:00) CPython 在类创建阶段(type_new)会为声明的每个 slot 计算相对于对象基地址的固定内存偏移量(offset),并将槽指针连续追加在 C 结构体末尾。 (2:37) 在 PyType_Ready 阶段,CPython 会在类字典中为每个 slot 生成 Member Descriptor,使得属性访问直接转化为基地址加偏移量的快速指针寻址,完全省去实例哈希表的检索开销。 (6:50) 使用 __slots__ 的类默认不再为实例创建 __dict__ 字典,彻底消除了动态哈希表的内存预留与冗余开销,实现“一个萝卜一个坑”的内存紧凑布局。 (9:56)

Reading summary

码农高天指出,Python 中的 `__slots__` 机制不仅能通过白名单增强代码鲁棒性,还能将属性赋值访问性能提升约 10% 并显著压缩内存,其本质是将动态哈希表查找改造为类级描述器与连续 C 结构体的指针偏移寻址。 属性白名单与性能内存表现 (0:00) 在 Python 类定义中声明包含属性名称的元组或列表形式的 `__slots__`,可直接限制实例允许绑定的属性种类。当尝试为实例赋予未声明的属性时,解释器会立即抛出 `AttributeError` 异常,起到白名单约束作用。除规范属性范围外,基准测试显示,带有 `__slots__` 的类在属性赋值操作上的运行速度相比普通类提升约 10%,并大幅降低了内存开销。 类创建阶段的连续内存布局改造 (2:37) 在 CPython 底层的 `type_new` 函数中,类创建时会对 `__slots__` 定义进行解析,滤除 `__dict__` 与 `__weakref__` 并将属性名存入 `ht_slots`。Python 对象在 C 层面表现为结构体,声明 slots 会在类原本的基准内存大小(`tp_basicsize`)末尾连续追加固定空间。系统为每个 slot 构建类型为 `T_OBJECT_EX` 的 `member` 结构体,首个 slot 偏移量对齐基准大小,后续 slot 依序递增一个 `sizeof(PyObject*)`(64 位系统下通常为 8 字节),并将元数据数组指针记录在类型的 `tp_members` 中。 Member Descriptor 与指针偏移寻址 (6:50) 在类初始化的 `PyType_Ready` 阶段,CPython 调用 `add_members` 遍历成员元数据,为每个 slot 生成成员描述器(Member Descriptor)并注册进类的 `__dict__`。依据 Python 的属性解析顺序(MRO),访问实例属性时会优先检查类层级的描述器。底层 C 函数 `PyMember_GetOne` 直接将实例对象的绝对基地址加上预设的固定偏移量,通过纯粹的指针算术定位内存并返回对象指针,完全跳过了运行时的动态解析。 哈希表剥离带来的效率优势验证 (9:56) 普通类实例在未命中类描述器时,必须回退到实例私有的 `__dict__` 哈希表中进行二次查找,且动态扩容的哈希表需要保留冗余空间以维持查询性能。相比之下,使用 `__slots__` 的类直接在首次 MRO 检查时命中描述器完成取值。代码验证表明,使用 `__slots__` 的类在其类字典中具有对应的 `member_descriptor`,其实例默认完全不生成 `__dict__` 字典,属性以“一个萝卜一个坑”的形式紧凑存储在连续内存中,从而实现了性能与内存的双重优化。 通过剖析 CPython 源码可见,`__slots__` 的优化完全建立在以固定的 C 结构体内存偏移替代动态哈希表的机制之上;其实例默认不具备 `__dict__`,在获得确定性与效率的同时,也彻底放弃了普通对象的动态扩展特性。

Source quotes

[9:56] 码农高天: 如果你用 slots 的话,在每一个 object 上都是一个萝卜一个坑;如果你不用 slots 的话,你需要用一个 dictionary 来保存这些萝卜。 [0:00] 码农高天: slots 这个机制实际上至少给我们带来了三件事:第一,通过限制属性的种类,利用白名单这个制度让我们的代码更有鲁棒性;第二,在 access 这些属性的时候速度更快;第三,实际上它是更省内存的。

Original transcript

[0:00] 这期视频我们来讲一下slotsslots是可以定义在class的definition里面的一个特殊的变量一般情况下它是一个cupot或者是一个list它的作用是限制这个类的实力里可以使用的属性的种类这么说比较复杂我们看一个例子我们看左边的代码我定义了一个class A然后它的slots我给了两个值一个F一个G都是string这个时候呢 我建立了一个新的Object O然后我做一个O点B等于2这个O.B=2对于我们平时定义的class来说就是一个很平常的事情就是把O这个object里面B这个属性扶持成2我们运行一下这代码可以看到这里报错了他说A这个object没有attribute B这里报错的原因就是我们通过slot这个特殊的属性告诉了 PythonA这个class里面就只能使用F和G这两个属性我们看我们把左边刚才的O.B等于2改成O.F等于2这个时候程序就能正常的运行了因为F是我们定义的slot 里面其中的一个名字那这件事从使用的角度上还是挺清晰的对吧我们给几个属性的名字告诉他你只能用这些属性有点像一个白名单但是只讲到这儿显然不是我们视频的风格我们来看下面这个例子我们定义了两个class A跟BA这个class呢,我们定义了slots而B这个class我们什么都没有定义后面我们建立了两个实例 O和 P 分别是 A和 B 这两个class object下面我们做了一个benchmark我们分别运行非常多次O.f=2和相同次数的p.f=2,然后做一个计时为了减小误差我运行了这个程序非常多次大家可以看到O.f=2是显著的比p.f=2要快的这里大约是快了10%那实际上在使用slot的情况下,不光速度是更快的,而且它会更节省内存所以说slots这个机制实际上至少给我们带来了三件事第一通过限制属性的种类利用白名单这个制度让我们的代码更有鲁邦性第二在access这些属性的时候速度更快第三实际上它是更省内存的那熟悉我视频风格的朋友肯定就已经知道了接下来我们就要去研究一下这到底是为什么呢或者说Cpython是怎么实现这件事的呢那有关slots的很多魔法是在建立这个class的时候发生的所以我们首先要关注这个type new函数也就是在我们建立一个新的class或者说一个新的type的时候发生了什么首先在这里我们把这个下路线slots对应的这个值先存到了sloth这个变量里接下来把这个slot变成了一个tuple再往下呢是进行了一些基本的检查还有特殊值的处理这里的特殊值是dict这个slot跟weekref这个slot我们这期视频暂时不涉及他们俩 未来有机会再讲然后把这两个特殊的slot拿掉剩下的slot名字放到一个list里面把这个list的排序再保存回slots这个tuple里接下来对于我们新建的type把slot的tuple保存到HC slot里好 那下面就是魔法开始发生的地方首先他做了一个py heap type get members我们知道所有的python object 包括这个type object本质上就是一个C的Struct那这件事实际上就是拿到这个C struct特别后面的一个指针在我们给这个ET这个type分配内存的时候我们已经计算进去了,我们需要额外拿几个slot的内存。你可以想象一个正常的type是一个定常的C struct你每额外多一个slot你在申请内存的时候你就要多申请一块内存放在这个C struct的后面那么我们看到的这个TP basic size就是这个type正常的size所以这个getMembers是个macro他最后返回的是直到最后的一个指针这个指针就是我们保存type的member信息也就是slot信息的地方那对于美 [5:00] 一个slot我们相当于是给他建立一个member我们把他的name,把他的type和把他的offset都给复制到这个member的struct里这里注意两点,第一,它的type永远是tObjectEx这个东西等会我们会用的到这个代表保存到这个slot里面的东西是一个py object第二个就是这个 offset这个offset里面保存了这个slot offset这个slot offset是什么呢我们看它最开始是这个Base的TP Basic Size也就是它是建立的那个object对象正常C-Struct的Size这里的 Slot Offset是一个指针偏移量就是保存我要在什么相对位置去保存我这个slot里面存的这个object我们看对于每一个slot都会把slot offset加一个pyObject*的size这里比较难懂啊会给大家举一个例子比如说我的class A在正常情况下需要100个byte来保存那它最开始的slot offset就是100也就是在每一个class A的object需要的这100个byte后面我开始一个一个放他slot需要的空间那第1个slot位置就在100第二个swat可能在108第三个slot可能在116你可以理解为在每一个新建的object上面我都给这些slot配备了专属的空间比如刚才的例子我们有两个slot一个叫F一个叫G那实际上就是我说F你的指针存在100这个位置G你的指针存在108这个位置然后我把100和108存到这个MP offset里我记住这两个数在做完了这些事之后我们把这个指向Members的指针给保存到typeTPMembers里那到目前为止我们实际上只是在这个class或者说在这个type上面保存了一些跟slot有关的数据包括slot的名字是什么还有这个slot的offset是什么我们并没有影响它运行时候的behavior所以说魔法还在继续我们就要看到这个PiTypeReady函数里这个py type ready函数就是我们每一次在新建一个type或者新建一个class之后一个收尾函数那在这里最相关的就是这段程序他运行了一个ad members在这个AdMembers函数里对于每一个刚才我们建立的member数据他都做了两件事首先,他建立了一个descriptor描述器这个是member的描述器第二 他把这个member的描述器给放到了class这个dict里这会导致什么呢回忆一下我们之前学过的跟描述器有关的内容当我们在一个class A 的 object O 里面调用 O.f 的时候他首先会去他的MRO里面找到F对应的descriptor那找他MRO的时候就是找class A的Dict所以当你在尝试access O.F的时候他就会调用这个我们刚刚建立的描述器我们来看一下这个描述器这里是描述器的type可以看到这个描述器有它自己的get和set函数这两个函数就是我们在尝试读取或者保存属性的时候会使用到的函数我们简单看一下get这个get函数的核心内容是在这个pymemberget1里的注意一下这个object就是刚才我们说的object o是这个class的一个实例而这个DMember是我们刚才保存到type里面那个Member就是保存着它的名字和它的offset的那个值get1函数上来在这里把这个address加了一个offset这一步如果你还能回忆起来我们之前讲的内容的话就是找到这个object应该保存到哪个地址刚才我们说F在地质100对吧 G在地质108这个ADDR呢保存的是这个object本身的绝对地址绝对地址加上 offset就是我们要找的这个slot里面保存的这个object [10:00] 地址我们之前提到了slot是tobjectex所以我们要把这个地址给Cath成一个pyObject然后返回到了这里 我们的魔法就正式完成了简单的总结一下呢他大概经历了这么几个步骤首先通过你定义的slots在这个type里面保存了一些数据分别是这些slots的名字和他们在建立的实例里面对应的相对位置然后根据这些数据建立了描述器把这个描述器保存在这个type的dict里面最后每一次建立实力的时候都额外流出几个slots的空间然后通过这些描述器在这些空间上进行读取那这样为什么会让速度更快呢我们在之前的视频里提到过当我们正常的从一个object里面尝试拿到一个attribute的时候他首先要去找这个 attribute 在mro里面对应的 descriptor就无论如何他要在他的class里面还有他的父辈的class里面找找有没有这个东西这一步是必须的然后他再在自己object的dict里面找好如果使用slots的话他会在找描述器的时候就找到正确对应的描述器并且这个描述器会直接告诉你这个变量保存的地点在哪儿。你就直接可以通过这个描述器拿到这个变量了但是如果你不用slots的话 你相当于就要用这个object的dict也就是你要再经历一次这个dictionary的寻找过程那显然这就是一笔额外的开销在内存上也是如此如果你用slots的话在每一个object上都是一个萝卜一个坑如果你不用slots的话 你需要用一个dictionary来保存这些萝卜但是我们知道当我们用字典保存数据的时候我们实际上需要的内存是比这些数据的量还要大一些的这样才能保证平均OE的access速度所以说使用slots无论在速度上还是在内存的使用上都是有优势的好 那我们回到python来验证一下我们刚才说的事对于class A和class B,我们分别打印他们的相应相应dent可以看到在class A 里那是有 F 和 G 这两个 descriptor 的而在class B里是没有的同样的在复制之后我们可以尝试打印一下这两个实例的Dict可以看到P的实力里边是保存了F2这个key value pair的而O这个实例是没有Dict的因为他f的这个值并没有保存到他的dict里他压根就没有dict它是保存到一个他已经事先预留好的空间的好 这可能是整个B站讲的最详细的slust教程了希望对大家有所帮助如果你喜欢的话记得点一下关注别忘了一家三连哦字幕志愿者 杨茜茜