跳转至

Agent 消息对象用了 slots 之后,发现子类继承它时内存优化失效,怎么排查和修复?

🔍 出现这个现象,十有八九是子类默默地生成了 dict,把父类辛苦省下的内存又吃回去了。


🧬 根本原因:继承链上丢了 slots

看个典型错误:

class BaseMessage:
    __slots__ = ('role', 'content')

class ChatMessage(BaseMessage):
    def __init__(self, role, content, timestamp):
        self.role = role
        self.content = content
        self.timestamp = timestamp  # ❌ 这里会报错,因为没有槽位

发现报错,于是有人这样“修”:

class ChatMessage(BaseMessage):
    pass   # 没定义 __slots__

msg = ChatMessage()
msg.role = "user"
msg.content = "hello"
msg.timestamp = 123   # 竟然能跑!但 __dict__ 已经悄悄回来了

当子类没有定义 slots,Python 会自动为它创建 dict,即使父类有 slots 也白搭。父类的槽位仍然有效,但子类多出来的属性全跑进 dict,优化瞬间失效。


🩺 如何排查

# 1. 直接看实例有没有 __dict__
msg = ChatMessage()
print(hasattr(msg, '__dict__'))   # True → 内存泄漏实锤

# 2. 对比内存占用
import sys
base = BaseMessage()
base.role = "u"; base.content = "c"
print(sys.getsizeof(base))   # 比如 56

child = ChatMessage()
child.role = "u"; child.content = "c"; child.timestamp = 123
print(sys.getsizeof(child))  # 可能会大很多,且包含字典开销

# 3. 检查类的 __slots__ 是否被继承
print(ChatMessage.__slots__)  # 如果只打印父类的,没包含新属性,甚至没 __slots__ 属性

如果看到 dict 存在,且 slots 里没有声明新增属性,问题就定位到了。


🛠️ 修复方案

✅ 正确做法:子类显式定义 slots,只写新增属性

class ChatMessage(BaseMessage):
    __slots__ = ('timestamp',)   # 只写新增的,不要重复 'role', 'content'

    def __init__(self, role, content, timestamp):
        self.role = role
        self.content = content
        self.timestamp = timestamp

这样内存布局就会变成:父类的槽 + 子类的槽,一个字典都不会有。

🔁 如果你用 dataclass(slots=True),继承也要小心

from dataclasses import dataclass

@dataclass(slots=True)
class BaseMessage:
    role: str
    content: str

@dataclass(slots=True)   # 子类也一定要加 slots=True
class ChatMessage(BaseMessage):
    timestamp: int = 0

如果子类忘了 slots=True,问题完全一样。


📌 特殊情况:需要 weakref

如果项目中用了弱引用(比如大量消息对象被缓存追踪),记得在 slots 里加上 'weakref',否则弱引用会失效或报错。

class BaseMessage:
    __slots__ = ('role', 'content', '__weakref__')

子类不用重复加,weakref 会自动沿继承链生效。


🧪 验证修复效果

msg = ChatMessage("user", "hello", 1234567890)
print(hasattr(msg, '__dict__'))  # False → 优化生效
print(sys.getsizeof(msg))        # 与预期槽位大小一致,无字典膨胀

一句话总结修复规则:只要继承链上任何一个类没定义 slots,就会在那一刻产生 dict,内存优化就断了。 所以每个子类都要显式写出自己的 slots,哪怕它是空的(slots = ()),用来“封印”字典的生成。