想象一下,你正在搬家。普通 Python 实例对象就像一个没有分隔的大纸箱,想塞什么就塞什么——今天塞进一个旧相册,明天加叠一件毛衣,箱子越来越重,搬家师傅满头大汗。而 __slots__ 就像定制好的带隔板行李箱,每个物品有固定卡位,放不下多余的东西,但箱子轻便、搬运飞快。 本文带你认识 Python 中这个“减肥神器”__slots__,以及它的好搭档 dataclasses(数据类,一种帮你自动生成样板代码的“打包清单工具”)。我们用一个贯穿全篇的“搬家装箱”比喻,从内存机理讲到实战对比,让你彻底明白如何给 Python 程序“瘦身”。

岗位关联

掌握 __slots__ 与 dataclasses,你就能在实战中精准定位内存瓶颈,写出既简洁又高效的 Python 代码——这是从初级脚本迈向工程化开发的关键一步。

目录
  1. 可视化演示
  2. 记忆机制
  3. 属性访问
  4. slots定义
  5. 内存优化
  6. dataclasses
  7. 对比选型
  8. 综合实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

实例内存占用对比 普通类 (with dict)Total: ~400 bytes/instanceslots 类动态添加属性:obj.new_attr = value ✓动态添加属性:obj.new_attr = ✗ AttributeErro...

记忆机制

搬家时,普通纸箱没有任何固定结构,你想塞什么就塞什么——旧台灯、书、毛绒玩具,全堆在一起。Python 中普通实例对象就是这样的“大纸箱”:每个实例内部都有一个名为 __dict__ 的字典(一个可动态增删的“万能口袋”),你给它赋什么属性,它就往口袋里塞什么键值对。

这个万能口袋非常灵活,但代价很大。字典为了支持快速查找,内部维护了哈希表(一种用“编号抽屉”加速查找的结构),每个键值对还额外存储哈希值、键引用、值引用等元信息。粗略估算,每个实例光这个口袋就要占用 64 字节以上,属性越多开销越大。

对比之下,类属性(定义在类上的变量)是所有实例共享的“共用仓库”,只存储一份。真正浪费内存的是每个实例各自背着的那一整套 __dict__ 字典——当你有十万个实例,就是十万个重复的“万能口袋”,内存自然爆表。

回到搬家场景:如果想省空间,最直接的办法就是扔掉那些可有可无的“万能口袋”,给每个箱子装上固定隔板——这正是 __slots__ 的核心理念。

内存(N)=N×(sizeof(instance)+sizeof(__dict__))≈N×96B\text{内存}(N) = N \times (\text{sizeof}(\text{instance}) + \text{sizeof}(\_\_dict\_\_)) \approx N \times 96B
\(N\) 为实例数量,\(\text{sizeof}(\text{instance})\) 为实例基础大小,\(\text{sizeof}(\_\_dict\_\_)\) 为每个实例的字典开销。实例数越大,字典开销越凸显。
  1. 实例数量 \(N\)表示你创建了多少个对象。每多一个实例,就多一份内存负担。
    NN
  2. 实例基础大小Python 对象本身要存储类型指针、引用计数等固定信息。
    sizeof(instance)\text{sizeof}(\text{instance})
  3. 字典开销每个实例的 __dict__ 为支持快速键值查找,额外占用哈希表结构。
    sizeof(__dict__)\text{sizeof}(\_\_dict\_\_)
  4. 总内存两者相加再乘以实例数量,就是整体内存占用。
    N×(sizeof(instance)+sizeof(__dict__))N \times (\text{sizeof}(\text{instance}) + \text{sizeof}(\_\_dict\_\_))

属性访问

带着大纸箱搬家时,找一件物品非常费劲:你得翻遍整个箱子。Python 访问实例属性 obj.x 时,底层会调用 __getattribute__ 方法(一个控制属性读取的“管家”),管家先去实例的 __dict__ 字典里按名字“翻找”。

字典查找用的是哈希:先对属性名做哈希计算,找到桶号(存放位置的编号),再逐个比对键。这个过程虽然经过了高度优化,但本质上仍是“翻箱倒柜”。加上每次访问都有一层方法调用包装,性能自然打了折扣。

而使用 __slots__ 后,属性不再存放在字典里,而是变成描述符(descriptor,一种实现对属性访问控制的协议)。描述符有点像给每个隔板贴上固定标签,Python 内部直接通过偏移量(类似“第 3 个抽屉”)取属性,完全跳过哈希查找。此外,__slots__ 生成的描述符也定义了 __set__ 方法(赋值时的拦截器),赋值同样走快速路径。

所以,固定隔板行李箱不仅省空间,存取物品也更快——因为你知道每件东西的固定位置,不需要“翻箱倒柜”地找。

tlookup=thash+tprobe,tslot=toffsett_{\text{lookup}} = t_{\text{hash}} + t_{\text{probe}}, \quad t_{\text{slot}} = t_{\text{offset}}
\(t_{\text{lookup}}\) 为字典查找耗时,需先算哈希再探测桶;\(t_{\text{slot}}\) 为槽位读取耗时,直接按偏移量取内存。
  1. 哈希计算 \(t_{\text{hash}}\)对属性名字符串计算哈希值,得到候选桶位置。
    thasht_{\text{hash}}
  2. 哈希探测 \(t_{\text{probe}}\)哈希冲突时需要在附近桶中逐个寻找(探测)。
    tprobet_{\text{probe}}
  3. 槽位偏移 \(t_{\text{offset}}\)__slots__ 属性在实例内存中的位置在类定义时就固定了,直接按偏移读取。
    toffsett_{\text{offset}}

slots定义

__slots__ 的用法非常直观:在类中声明一个类属性,列出允许的实例属性名。就像给行李箱装上明确的隔板清单,写明了哪些格子允许放东西。定义之后,这些属性会作为槽位固定下来。

这样定义后,Point 实例不再拥有 __dict__ 字典,只能存放 x 和 y 这两个属性。如果你尝试 p.z = 3,Python 会直接抛出 AttributeError(属性错误)——如同往固定隔板的箱子里硬塞一个不存在的格子,箱子说“不,没有这个卡位”。

但需要注意:如果子类没有定义自己的 __slots__,子类实例会默认重新获得 __dict__!这就像你买了一个带隔板的行李箱,结果又在外面套了一个大塑料袋,所有空间优势瞬间消失。

所以,使用 __slots__ 前要明确:我就是不希望对象随意增删属性,我愿意用灵活性换取更小的内存和更快的访问。

slots={a1,a2,…,ak},∀实例仅存 ai\text{slots} = \{a_1, a_2, \ldots, a_k\}, \quad \forall \text{实例仅存} \ a_i
类的实例属性集合被限制为 __slots__ 中列出的属性名,实例中不再有动态字典存储。
  1. 属性集合 \(a_1, \dots, a_k\)__slots__ 中列出的每个名字都成为允许存放的属性。
    {a1,a2,…,ak}\{a_1, a_2, \ldots, a_k\}
  2. 实例仅存这些属性除了元信息,实例内存只预留这 \(k\) 个槽位,无法新增其他属性。
    ∀实例仅存 ai\forall \text{实例仅存} \ a_i
python6 行
class Point:    __slots__ = ('x', 'y')     def __init__(self, x, y):        self.x = x        self.y = y

内存优化

__slots__ 最吸引人的地方在于内存优化效果显著。它直接移除了实例内部的 __dict__ 字典,这一步就省下了每实例约 64 字节的开销(字典对象自身)。加上属性改为紧凑排列的槽位,实例总内存可以下降 40%–60%。

我们做一个简单的基准测试:创建 10 万个普通类实例与 __slots__ 实例,用 sys.getsizeof(Python 内置函数,获取对象内存大小)对比单实例内存,用 tracemalloc(内存追踪模块)统计总内存峰值。结果通常显示,__slots__ 实例总数内存只有普通实例的 50%–60%。

为什么差距这么大?因为普通实例除了字段本身,还要维护哈希表结构、预留扩容空间(字典会预分配额外内存以减少哈希冲突)。这些“隐形重量”在实例数量庞大时被急剧放大。数据管道、游戏实体、配置对象等动辄十万百万的实例场景,__slots__ 能节省数 MB 到数百 MB 内存。

如果你正在处理百万级对象,这就是节省 64MB 的差距。固定隔板行李箱的单箱重量优势,在集装箱码头(大规模数据场景)被放得格外明显。

M普通=N×(C+64+8d),Mslots=N×(C+8d)M_{\text{普通}} = N \times (C + 64 + 8d), \quad M_{\text{slots}} = N \times (C + 8d)
\(C\) 为对象固定头大小,\(d\) 为字段个数。普通实例额外背负 64 字节的 __dict__ 基础开销。
  1. 固定头 \(C\)每个 Python 对象都有的类型指针、引用计数等信息,无法省去。
    CC
  2. 字典基础 64B一个空字典也要占 64 字节,为了动态性这是最低代价。
    6464
  3. 字段存储 \(8d\)每个槽位存一个 8 字节的指针,普通实例存在字典里本质上也是存指针,但多了哈希表开销。
    8d8d
  4. 总内存差两者相减,每个普通实例都要多付 64 字节的“字典税”。
    M普通−Mslots=64NM_{\text{普通}} - M_{\text{slots}} = 64N

dataclasses

dataclasses 模块是 Python 3.7 引入的“打包清单生成器”。配合 @dataclass 装饰器,你只需声明字段和类型注解,模块自动帮你生成 __init__(构造方法)、__repr__(打印字符串)、__eq__(相等比较)等方法。就像搬家时贴好标签的清单,每件物品都有了标准化的登记流程,省去手写样板代码的枯燥。

dataclasses 内部最关键的机制之一是 __dataclass_fields__ 字段:它是一个类级别的字典,保存了每个字段的 Field 对象元信息——字段名、类型、默认值、是否参与比较等。这在运行时非常有用,ORM(对象关系映射框架)、序列化器(把对象转成 JSON 等格式的工具)、配置系统都可以通过读取它来自动处理字段。

但要注意一个关键事实:默认的 dataclass 仍然是普通 Python 类,实例依然带着 __dict__ 字典。换句话说,@dataclass 只帮你省了写代码的时间,不会省内存。

dataclass 是聪明的“打包清单”,但它默认用的是普通纸箱——用 __slots__ 给它换上固定隔板行李箱,才能内存、代码双丰收。

Dataclass(C)=Class(C)+∑f∈fieldsField(f),__dict__ 依然存在\text{Dataclass}(C) = \text{Class}(C) + \sum_{f \in \text{fields}} \text{Field}(f), \quad \_\_dict\_\_ \text{ 依然存在}
dataclass 是在普通类基础上增加字段元数据与自动方法,本质仍是普通对象,内存布局未改变。
  1. 字段集合 fields__dataclass_fields__ 把所有字段名收集起来。
    ∑f∈fields\sum_{f \in \text{fields}}
  2. 字段元信息 Field记录默认值、类型、比较规则等附加数据。
    Field(f)\text{Field}(f)
  3. 字典仍存在dataclass 不改变内存布局,__dict__ 保留。
    Dataclass(C)=Class(C)\text{Dataclass}(C) = \text{Class}(C)

对比选型

现在你已经认识了两种方案:__slots__ 提供内存优势和访问性能,但牺牲了动态灵活性;dataclasses 提供代码简洁性和丰富的元数据,但默认不优化内存。对比选型要基于实际场景。

内存方面,__slots__ 类实例大小约为普通类实例的 45%–60%,在大量实例场景优势巨大。灵活性方面,普通类和 dataclass 允许任意动态添加属性,__slots__ 则严格禁止。序列化方面,dataclass 可以轻松配合 asdict()(转换成字典的辅助函数)转成字典再 JSON 序列化,但 __slots__ 实例没有 __dict__,如果用 vars(obj) 或 obj.__dict__ 取属性会直接报错,需要另写“从槽位提取字段”的方法。

选型建议:需要高灵活轻量脚本,选普通类或 dataclass;需要高性能大规模实例,选 __slots__;想要两者兼得,就定义 slotted dataclass——在 dataclass 中设置 __slots__,Python 3.10 之后支持 @dataclass(slots=True) 一行搞定。

选型就像搬家前的整体规划:如果只是短期住宿(原型脚本),大纸箱随便塞;如果要长期定居(生产系统),固定隔板加打包清单才是最优解。

内存: Slots<Dataclass≈普通类,灵活性: Slots<Dataclass≈普通类\text{内存: Slots} < \text{Dataclass} \approx \text{普通类},\quad \text{灵活性: Slots} < \text{Dataclass} \approx \text{普通类}
内存与灵活性往往是跷跷板:追求内存最小化则限制动态属性,追求灵活则承担更高内存开销。
  1. 内存排序Slots 内存最小,dataclass 与普通类相同。
    Slots<Dataclass≈普通类\text{Slots} < \text{Dataclass} \approx \text{普通类}
  2. 灵活性排序Slots 受限,dataclass 与普通类同样自由。
    Slots<Dataclass≈普通类\text{Slots} < \text{Dataclass} \approx \text{普通类}

综合实战

理论讲完,进入实战。最佳实践是把 dataclasses 的语法糖与 __slots__ 的内存优化结合起来。

这样既获得了自动生成的 __init__、__repr__ 等方法,又能享受 __slots__ 带来的内存节省。对老版本 Python,可以手动在 dataclass 中写 __slots__,但要注意字段名必须一致。

实战中还有两个关键陷阱:第一,继承链中如果某个基类没有 __slots__,那么最派生类实例仍然会有 __dict__,内存优化会“前功尽弃”;第二,定义了 __slots__ 的类默认不支持 weakref(弱引用,一种不阻止对象被回收的引用机制),除非在 __slots__ 中显式加入 '__weakref__'。

最后是完整的基准测试方法论:使用 tracemalloc 追踪内存峰值,创建批量实例进行比较;对于单实例大小用 sys.getsizeof;想了解具体字段占用可用 pympler.asizeof(一个递归计算对象真实大小的库)。

搬家工程到今天圆满结束:从大纸箱换成固定隔板行李箱,再用打包清单统一标记——之后每一次搬运、每一家“分店”(新实例)都轻装上阵。

优化后内存=N×(基础头+8×∣slots∣)\text{优化后内存} = N \times (\text{基础头} + 8 \times |\text{slots}|)
大量实例场景下,内存与实例数线性相关,__slots__ 从常数项上直接砍掉了 __dict__ 的重量。
  1. 基础头类型指针与引用计数等固定开销,任何对象都无法避免。
    基础头\text{基础头}
  2. 槽位数 \(|\text{slots}|\)__slots__ 中声明的属性个数,每个槽位占 8 字节指针。
    ∣slots∣|\text{slots}|
  3. 总内存线性公式显示,只需按实例数量乘上单实例成本即可估算。
    N×(基础头+8×∣slots∣)N \times (\text{基础头} + 8 \times |\text{slots}|)
python6 行
from dataclasses import dataclass @dataclass(slots=True)  # Python 3.10+,自动为 dataclass 添加 __slots__class Point:    x: float    y: float

AI 应用场景

大规模 AI 推理特征存储

阶段:数据预处理与特征工程

解决的问题:AI 推理中需要加载百万级用户特征向量,普通对象每个实例携帯 __dict__ 字典,内存占用巨大。使用 __slots__ 将每个特征对象的固定字段(如用户 ID、特征数组、更新时间)声明为槽位,可减少 40%-60% 内存占用,让缓存或检索服务在同样内存下承载更多特征。

具体案例:某推荐系统需在内存中常驻 500 万用户特征对象,每实例含 12 个特征字段。使用 @dataclass(slots=True) 后,内存从约 2.1GB 降至约 1.1GB,同时特征访问速度因偏移量读取提升约 20%,线上内存告警解除。

强化学习环境的状态封装

阶段:模拟环境与状态管理

解决的问题:强化学习训练中,环境状态对象被频繁创建和销毁,若使用普通类会导致大量短生命周期对象占用内存并触发频繁 GC。用 __slots__ 定义状态结构,配合对象池复用,可大幅降低内存分配频率。

具体案例:一个机器人仿真环境每秒创建约 20 万个状态对象。改用带 __slots__ 的不可变状态类后,仿真速度提升 35%,GC 暂停时间显著减少,训练吞吐量提高。

数据验证管线与配置管理

阶段:模型服务部署与 Python 工程化服务配置

解决的问题:模型服务的配置对象散落大量字段,dataclass 的 __dataclass_fields__ 机制可以通过元数据统一校验和序列化,而 __slots__ 保证配置在加载后不可被随意修改,防止运行时非法赋值。

具体案例:一个推理服务使用 slotted dataclass 管理模型路径、批处理大小、阈值等配置项。读取效果:配置被非法修改时立刻抛出 AttributeError,配合 asdict() 转换字典后做版本记录,上线半年零配置事故。

Python 演示

python56 行
import sysimport tracemallocfrom dataclasses import dataclass, asdict  class NormalPoint:    def __init__(self, x: float, y: float):        self.x = x        self.y = y  class SlotPoint:    __slots__ = ("x", "y")     def __init__(self, x: float, y: float):        self.x = x        self.y = y  @dataclass(slots=True)class DataSlotPoint:    x: float    y: float  def benchmark():    N = 100_000    print("单个实例内存对比:")    np_, sp_, dp_ = NormalPoint(1.0, 2.0), SlotPoint(1.0, 2.0), DataSlotPoint(1.0, 2.0)    print("  NormalPoint:", sys.getsizeof(np_), "bytes")    print("  SlotPoint:", sys.getsizeof(sp_), "bytes")    print("  DataSlotPoint:", sys.getsizeof(dp_), "bytes")     tracemalloc.start()    _ = [NormalPoint(1.0, 2.0) for _ in range(N)]    current, peak = tracemalloc.get_traced_memory()    print("NormalPoint 总内存峰值:", peak / 1024 / 1024, "MB")    tracemalloc.stop()     tracemalloc.start()    _ = [SlotPoint(1.0, 2.0) for _ in range(N)]    current, peak = tracemalloc.get_traced_memory()    print("SlotPoint 总内存峰值:", peak / 1024 / 1024, "MB")    tracemalloc.stop()     tracemalloc.start()    _ = [DataSlotPoint(1.0, 2.0) for _ in range(N)]    current, peak = tracemalloc.get_traced_memory()    print("DataSlotPoint 总内存峰值:", peak / 1024 / 1024, "MB")    tracemalloc.stop()  if __name__ == "__main__":    benchmark()    p = DataSlotPoint(1.0, 2.0)    print(asdict(p))
目的:通过 tracemalloc 和 sys.getsizeof 对比普通类、__slots__ 类、slotted dataclass 三者的内存占用,演示内存优化效果。
  1. class SlotPoint:在类中定义 __slots__ = ("x", "y"),声明属性白名单,实例不再拥有 __dict__。
  2. tracemalloc.start()启动内存追踪,记录 10 万实例创建过程中分配的内存峰值。
  3. [NormalPoint(1.0, 2.0) for _ in range(N)]批量创建普通实例,用列表推导快速生成大规模对象。
  4. sys.getsizeof(sp_)获取单个实例的基本内存大小,__slots__ 实例通常更小。
  5. @dataclass(slots=True)一行代码让 dataclass 自动生成 __slots__,兼顾语法糖与内存优化。
  6. asdict(p)将 dataclass 实例转换成字典,方便序列化;注意 __slots__ 实例没有 __dict__,不能直接用 vars()。

常见误区

误区“用了 `__slots__` 之后,类属性(如常量)也不能访问了,感觉太局限了。”

正解__slots__ 限制的是“实例属性”,即每个对象自己的属性存储空间。类属性存在于类对象上,所有实例共享,完全不受 __slots__ 影响。比如定义了 __slots__ = ('x',) 的类,依然可以设置类变量 cls.version = '1.0' 并正常读取,这就好比行李箱隔板限制了随身物品,但仓库里的公用物资照样能取用。

为何常见:初学者容易混淆“实例属性”和“类属性”的内存位置,误以为 __slots__ 把整个类都“锁死”了。

误区“用 `@dataclass` 装饰器后,代码更简洁,肯定内存也更小了吧?”

正解默认的 dataclass 仍是普通类,实例依然携带 __dict__ 字典,sys.getsizeof 与普通类实例基本一致。它优化的只是“写代码的时间”而非“运行内存”。要想让内存也优化,必须使用 @dataclass(slots=True)(Python 3.10+)或手动在类中定义 __slots__。

为何常见:@dataclass 给人的“自动化”印象容易让人误以为它连内存布局都一并优化了。

误区“既然 `__slots__` 是性能优化神器,那所有类都应该用它,越用越快。”

正解__slots__ 的主要收益是内存,属性访问性能提升幅度有限(通常仅在高频循环中可感知),而且如果类中存在大量动态属性操作,__slots__ 反而会频繁抛出 AttributeError 并破坏灵活性。在小型脚本、原型开发或需要动态扩展对象的场景,普通类更合适。

为何常见:把“内存优化”与“性能优化”混为一谈,忽略了访问速度和灵活性是不同维度的权衡。

误区“基类定义了 `__slots__`,子类肯定也是紧凑布局,不用再管了。”

正解如果子类没有声明自己的 __slots__,子类实例会默认重新生成 __dict__,内存优势消失。正确做法是子类也声明 __slots__ = () 或列出自己的属性名,并包含父类的槽位名(必要时)。这就像给行李箱加了一层外挂口袋,不主动指定,外挂空间就会被塞回来。

为何常见:许多人以为 __slots__ 像 C++ 的虚函数表一样可以自动继承,忽略 Python 文档中的明确说明。

误区“`__slots__` 实例没有 `__dict__`,JSON 序列化直接 `obj.__dict__` 会报错,所以不适合序列化场景。”

正解不能直接 vars(obj) 不代表不能序列化。可以手动遍历 __slots__ 中的字段构造字典,或用 dataclass 的 asdict(),再交给 JSON。几种方案都能实现序列化,只是你失去了“自动序列化所有实例属性”的便利,需要显式枚举字段。

为何常见:习惯了 json.dumps(obj.__dict__) 的写法后,面对 __slots__ 没有 __dict__ 的情况会误以为“不能序列化”,实则只是需要换一种提取方式。

面试题

Q1. 在类中定义 __slots__ = ('x', 'y') 后,以下哪个行为会发生?

单选题 · easy

  • A.实例可以任意添加新属性
  • B.实例不再有 __dict__ 字典← 正确答案
  • C.类属性全部失效
  • D.实例无法访问已有属性

答案:B。__slots__ 的核心是去掉实例的 __dict__,只保留声明的属性槽位。A 错误,正是为了禁止动态属性才用 __slots__;C 错误,类属性不受影响;D 错误,声明的属性可以正常访问。

Q2. 下列哪种类型默认在实例内部携带 __dict__ 字典?

单选题 · easy

  • A.使用 __slots__ 的普通类
  • B.Python 3.10 中使用 @dataclass(slots=True) 的类
  • C.普通 Python 类← 正确答案
  • D.带 __weakref__ 的 __slots__ 类

答案:C。普通 Python 类实例默认 __dict__ 存储实例属性;A 与 B 是优化后无字典;D 虽有 __weakref__ 槽位但无 __dict__。

Q3. @dataclass 装饰器的主要作用是?

单选题 · easy

  • A.自动生成 __slots__
  • B.自动减小实例内存
  • C.自动生成 __init__ 等方法← 正确答案
  • D.自动禁止动态属性

答案:C。@dataclass 根据字段注解自动生成 __init__、__repr__、__eq__ 等样板方法。A、B、D 都不默认发生,除非显式使用 slots=True。

Q4. 以下哪个选项是 __slots__ 的主要内存收益来源?

单选题 · mid

  • A.减少哈希冲突
  • B.去掉实例上的 __dict__ 字典← 正确答案
  • C.取消引用计数机制
  • D.把属性移到类属性共享

答案:B。__slots__ 省去了每个实例的 __dict__(空字典也要约 64B),并用固定槽位存放属性指针。A 是字典相关的问题,与 __slots__ 无关;C、D 说法不成立。

Q5. 定义 __slots__ 的类,要支持弱引用需要在槽位中加入什么?

单选题 · mid

  • A.'__dict__'
  • B.'__weakref__'← 正确答案
  • C.'__slots__'
  • D.'__module__'

答案:B。__slots__ 默认移除弱引用支持,要恢复需显式在槽位中包含 '__weakref__',让实例保留弱引用所需的内部指针。其他选项不是弱引用所需的特殊槽位名。

Q6. 关于 __dataclass_fields__,下列说法正确的是?

单选题 · mid

  • A.它是实例属性,保存运行时的值
  • B.它是包含每个字段 Field 元数据的类字典← 正确答案
  • C.它只在 Python 3.10 之后存在
  • D.它负责自动生成 __dict__

答案:B。__dataclass_fields__ 是类级别的字典,值为 Field 对象,记录字段名、类型、默认值等信息。A 错在它是类属性;C 错在 Python 3.7 引入 dataclass 就有;D 与生成 __dict__ 无关。

Q7. 基类定义了 __slots__ = ('x',),子类未定义任何 __slots__,那么子类实例会?

单选题 · hard

  • A.实例拥有紧凑布局,无 __dict__
  • B.实例有 __dict__,但只能存 x
  • C.实例有 __dict__,且可添加任意属性← 正确答案
  • D.创建子类实例时直接报错

答案:C。子类若未定义 __slots__,默认会生成 __dict__,因此所有属性(包括派生属性)都存进字典,内存优势丢失。A 错误;B 错误,有 __dict__ 后可以任意添加;D 错误。

Q8. 在 Python 3.10+ 中,如何用 dataclass 实现内存优化?

单选题 · hard

  • A.@dataclass 默认支持
  • B.@dataclass(slots=True)← 正确答案
  • C.@dataclass(dict=False)
  • D.@dataclass(frozen=True)

答案:B。@dataclass(slots=True) 让 dataclass 同时生成 __slots__,兼具代码简洁和内存紧凑。A 默认没有;C 是不存在的参数;D 只让实例不可变,不影响内存布局。

Q9. 下列哪些方式可以获取实例的字段数据,用于序列化?

多选题 · mid

  • A.直接访问 obj.__dict__← 正确答案
  • B.遍历 __slots__ 字段构造字典← 正确答案
  • C.使用 dataclasses.asdict(obj)← 正确答案
  • D.使用 sys.getsizeof(obj)

答案:ABC。A 对普通类有效,B 是 __slots__ 类需要的手段,C 是 dataclass 的推荐做法。D 只返回对象大小不提取字段数据。

Q10. 关于 __slots__ 的性能优化,以下说法正确的有哪些?

多选题 · hard

  • A.属性访问从字典查找变为偏移量读取← 正确答案
  • B.内存节省主要在省去每实例的 __dict__← 正确答案
  • C.所有情况下代码都会运行更快
  • D.支持弱引用需显式加入 __weakref__← 正确答案

答案:ABD。A 对,槽位属性通过描述符与偏移量直接存取;B 对,省字典是关键收益;D 对,默认移除弱引用支持。C 错,若大量动态属性操作,__slots__ 会抛异常且灵活性受损,并非“所有情况更快”。