📄 一句话概括
同样两个字段,普通类 344 字节/个、__slots__ 只要 48——字段怎么声明,直接决定占多少。
📝 本节和前面几节的关系
5.11 讲多态,这一节讲继承和声明方式怎么改变内存布局。
一、继承时字段表合并
子类的 __init__ 先调父类的,于是父类放的字段也进了子类实例的字段表。
实测字段表是 base/child 两个——父类 1 + 子类 1。
所以继承在内存上的第一个后果是:实例变胖了,多带父类的字段。
二、声明方式更影响大小
普通类:对象本体 48 字节,那本字段表 296 字节,合计 344 字节/个。
__slots__:只有本体 48 字节,没有字段表。
每个实例省下 296 字节——一万个实例就是 296 万字节,接近 3 MB。
同样两个字段,只因为声明方式不同,占了七分之一。
三、代价
不能动态加字段:slots 类里没有那本字典,多赋一个属性就报错。
继承时要重新声明:子类也要写 __slots__,否则又会带出一本字典。
所以它是【用灵活性换内存】,不是无脑优化。
四、什么时候值得
实例数量大(几万、几十万)——这时几百字节乘上去才叫量。 字段固定——不会临时加属性。 否则不要提前优化:多出来的那本字典,换来的是随便加字段的自由。
五、动手 Demo:根据继承的信息类型的不同,内存配置也不同
同样两个字段,一个普通类、一个用 slots,量它们各占多少。 再看继承之后字段表里多了什么。
class Plain:
def __init__(self, a, b):
self.a, self.b = a, b
class Slotted:
__slots__ = ("a", "b")
def __init__(self, a, b):
self.a, self.b = a, b
p, s = Plain(1, 2), Slotted(1, 2)
print(sys.getsizeof(p) + sys.getsizeof(p.__dict__))
print(sys.getsizeof(s))
实跑输出:
--------------------------------------------------------------------
根据继承的信息类型的不同,内存配置也不同
--------------------------------------------------------------------
同样的字段,**用不同的声明方式,对象占的内存不一样。**实测两种类:
普通类:对象本体 48 字节
普通类:那本字段表 296 字节
普通类:合计 344 字节/个
slots 类:只有本体 48 字节/个
每个省下 296 字节
普通类有字段表吗 True
slots 类有字段表吗 False
继承时又是一回事——**子类实例的字段表里,父类放的字段也在:**
子类实例的字段 base/child
字段数 2 个(父类 1 + 子类 1)
父类实例的字段 base
**同样两个字段,普通类每个实例要多付一本字段表的钱。**
**`__slots__` 就是把字典换成【固定几格】,代价是不能动态加字段。**
**所以【内存配置】不是抽象的:字段怎么声明,直接决定每个实例占多少。**
实测:普通类 48+296=344 字节/个,slots 类只有 48 字节/个,每个省 296。 继承时字段表还会合并(实测 base/child 两个);slots 的代价是不能动态加字段。
30 秒实验
- 把一个实例很多的类改成
__slots__,看报错在哪里。 - 数一数你的类有几层继承,估一下每个实例多背了多少字段。
常见误区
- 以为字段声明只影响写法。它直接决定每个实例占多少。
- 以为 slots 总是更好。它牺牲了动态加字段的自由。
自测三问(点开对答案)
1. 继承怎么影响内存布局? —— 子类先调父类 __init__,父类放的字段也进子类实例的字段表;实测 base/child 两个字段。 2. 普通类和 slots 类各占多少? —— 普通类 48+296=344 字节/个;slots 类只有 48 字节/个,每个省 296。 3. __slots__ 的代价是什么? —— 不能动态加字段,继承时子类也要重新声明,否则又会带出一本字典——是用灵活性换内存。- ☐ 下一节(5.13 孤立的实例由垃圾回收处理)
相关笔记
- OOP 的特征在于内存的用法 — 数据在堆上
- 进化的OOP结构之三:垃圾回收 — 谁负责回收这些块
- 🔷 C++基础 · 索引 — 对象布局与虚表
- 🧩 面向对象是怎样工作的 · 索引