享元模式本质是用WeakValueDictionary实现对象池与键值分离,确保内部状态可哈希稳定、外部状态由调用方传入,工厂为唯一入口,避免内存泄漏和复用失效。

享元模式不是“写个Flyweight类”,而是对象池 + 键值分离
直接继承 Flyweight 接口、定义抽象基类,对内存优化几乎没用。真正起效的是:用缓存(WeakValueDictionary)按内在状态查重,且确保每次请求都走同一工厂入口。
常见错误是把享元当单例用——比如为每种字体新建一个 Font('Arial', 12) 实例后就扔进全局变量,结果缓存不自动清理,对象越积越多;或者键用 dict 存,但没处理引用生命周期,导致本该回收的对象卡在内存里。
- 必须用
weakref.WeakValueDictionary做池子,不能用普通dict - 键必须可哈希且稳定,推荐
typing.NamedTuple或dataclasses.frozen=True包装内部状态 - 工厂方法必须是唯一入口,禁止客户端绕过工厂直接
Font(...)构造 - 享元类本身不能有
__del__,否则弱引用行为未定义
内部状态和外部状态划错,享元就失效
内部状态是“模板”,外部状态是“上下文”。划错的典型表现是:对象复用率极低,或复用后行为异常(比如改了一个地方的颜色,所有同字体文本全变色)。
例如渲染日志行:font_name、font_size、color 属于内部状态;而 line_number、content、timestamp 必须由调用方传入,绝不能塞进享元实例。
立即学习“Python免费学习笔记(深入)”;
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
- 判断标准:去掉这个字段,对象是否还能被识别为“同一类”?能 → 内部状态;不能 → 外部状态
- 字符串字面量如
"Consolas"可直接作键,但构造式如str.upper()结果不能直接用,需先归一化 - 浮点数慎作内部状态(精度误差导致键不匹配),优先转为
round(x, 2)或用整数单位(如字号用 pt * 10 表示)
Python里用 WeakValueDictionary 而不是 dict 的真实原因
dict 缓存享元,等于强持有对象引用;只要字典不删键,对象就永远不会被 GC。在长周期服务(如 Flask/FastAPI 后端、GUI 主循环)中,这极易引发内存缓慢泄漏——尤其当内部状态组合多、但实际并发使用少时。
WeakValueDictionary 只对值做弱引用,外部不再持有时自动剔除条目。但它有硬性限制:键仍需可哈希,且值不能定义 __del__。
- 初始化必须写成
self._pool = weakref.WeakValueDictionary(),别漏掉weakref. - 获取时用
self._pool.get(key),不要用in判断再取值(竞态风险) - 创建新实例后,必须显式赋值
self._pool[key] = new_instance,不能只存引用 - 测试时可用
len(factory._pool)观察缓存大小,配合gc.collect()验证回收
小规模场景加享元反而更慢
如果总共只生成几十个对象,或每个享元仅含 1–2 个 int / str 字段,加享元得不偿失。哈希计算、字典查找、弱引用簿记的开销会超过对象创建本身。
一个粗略分界线:对象总数 ≥ 1000 且内部状态组合数 ≤ 总数的 10%,才值得引入。比如渲染 5 万行日志,只有 7 种字体配置,缓存命中率会极高;但若每行字体都随机,缓存基本无效,还拖慢流程。
- 上线前务必压测对比:关掉享元工厂 vs 开启后的 P99 内存 RSS 和对象数(
gc.get_objects()统计) - 别在
__init__里做复杂计算——享元初始化应接近 O(1),耗时逻辑移至外部状态处理侧 - 调试时临时打印
id(flyweight),确认相同键确实返回同一对象
关键点始终是:缓存是否真被命中、外部状态是否真被剥离、弱引用是否真在起作用。这些没法靠代码结构推断,只能靠运行时观测。

















