备忘录模式的核心是隔离状态快照与原对象,由originator自主控制快照生成;正确做法是用@dataclass(frozen=true)定义只读memento类,显式声明字段以保障封装性、可维护性和类型安全。

备忘录模式的核心不是深拷贝,而是隔离状态快照与原对象
直接用 copy.deepcopy() 保存整个实例状态看似简单,但会带来两个实际问题:一是无法控制哪些字段该存、哪些不该存(比如临时缓存、文件句柄、线程锁);二是破坏封装——备忘录本应由原对象(Originator)自己决定如何生成快照,外部不该越权访问私有属性。正确做法是让 Originator 提供一个 save_to_memento() 方法,返回一个只读的 Memento 对象,且 Memento 的构造和内部数据对外不可变。
如何设计不可变的 Memento 类(Python 3.7+ 推荐使用 dataclass)
用普通类手写 __slots__ 或全属性只读逻辑容易出错;用 namedtuple 又缺乏类型提示和默认值支持。最简稳方案是 @dataclass(frozen=True):
@dataclass(frozen=True)
class EditorMemento:
content: str
cursor_pos: int
undo_stack_size: int
这样能天然阻止外部修改,且 EditorMemento 实例可安全存入列表或字典中用于撤销栈。注意:不要把可变对象(如 list、dict)直接塞进 frozen=True dataclass —— 它们内容仍可变,需提前转为 tuple 或用 copy.deepcopy() 在 save_to_memento() 内部处理。
撤销栈管理必须区分「保存时机」和「触发时机」
常见错误是每次用户输入都无条件调用 save_to_memento(),导致内存暴涨或撤销步数失控。真实场景中应:
- 在关键操作前保存(如用户按下 Ctrl+Z 后执行撤销前,先存当前态作为“重做”起点)
- 合并连续小操作(如连续输入字符,只在输入暂停 300ms 后存一次快照)
- 限制栈长度(如只保留最近 50 个
Memento,超出则pop(0)) - 避免在
__init__或 setter 中自动保存 —— 这会让初始化也计入撤销历史
为什么不用 pickle 而用显式字段序列化
用 pickle.dumps(obj) 存整个实例看似省事,但隐患明显:
- 反序列化时若类定义变了(字段删/改/重命名),
pickle.loads()直接抛AttributeError或静默丢数据 - 包含不可序列化对象(如
threading.Lock、socket.socket)会直接失败 - 无法做字段级 diff 或调试 —— 你只能看到一串乱码 bytes,没法快速确认某次快照里
cursor_pos是不是对的
显式声明 content、cursor_pos 等字段,既是契约也是文档。哪怕未来加新状态,也能明确知道要不要把它加入 EditorMemento。
真正难的不是写几个类,而是判断哪些状态属于「业务快照」、哪些只是「运行时中间态」。比如编辑器里「当前选中的语法高亮主题」该存,但「正在渲染的行号缓存」就不该进 Memento —— 后者属于性能优化副产品,重启或回滚后重新计算即可。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











