17、观察者模式实现(二):使用 Python 的 weakref 避免内存泄漏
上一节我们手写了一个观察者模式,功能是跑通了。但说实话,那个版本有个挺隐蔽的坑——内存泄漏。你可能觉得 Python 有垃圾回收,怎么还会泄漏?嗯,我当年也是这么想的,直到线上服务跑了三天,内存蹭蹭往上涨,我才意识到问题的严重性。
这一节,我们就来聊聊怎么用 Python 的 weakref 模块,把这个坑填上。
问题重现:强引用带来的“幽灵”对象
先看一个简单的例子。假设我们有一个主题(Subject),它内部维护了一个观察者列表。每个观察者都是一个对象实例。
class Subject:
def __init__(self):
self._observers = [] # 强引用列表
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
class Observer:
def __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} 收到: {message}")
表面上看没问题。但你想过没有:如果观察者对象被删除了,但主题还持有它的引用,会发生什么?
我举个例子。你在一个 GUI 系统中注册了一个窗口作为观察者。用户关掉了这个窗口,按理说窗口对象应该被回收。但因为主题还强引用着它,这个窗口对象就变成了“僵尸”——内存占着,但再也用不到了。
weakref 登场:弱引用的妙用
Python 的 weakref 模块,说白了就是允许你引用一个对象,但不增加它的引用计数。当对象只剩下弱引用时,垃圾回收器会毫不犹豫地把它收走。
我个人习惯用 weakref.ref 或者 weakref.WeakSet 来管理观察者。这里我推荐 WeakSet,因为它自动处理重复添加和移除不存在的元素,省心不少。
list 换成 weakref.WeakSet,这样当观察者对象被外部删除时,它会自动从集合中消失。
改造后的代码实现
import weakref
class Subject:
def __init__(self):
# 使用 WeakSet 替代 list
self._observers = weakref.WeakSet()
def attach(self, observer):
self._observers.add(observer)
def detach(self, observer):
# WeakSet 的 discard 不会抛异常
self._observers.discard(observer)
def notify(self, message):
# 遍历时,已经死亡的对象会自动跳过
for observer in self._observers:
observer.update(message)
class Observer:
def __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} 收到: {message}")
def __del__(self):
print(f"{self.name} 被销毁了")
注意看 __del__ 方法,我是为了演示才加的。实际项目中别依赖它,因为它的调用时机不确定。
验证效果:内存真的被释放了
我们来跑个测试:
def test():
subject = Subject()
obs = Observer("观察者A")
subject.attach(obs)
subject.notify("第一条消息")
print("删除观察者...")
del obs # 手动删除观察者
subject.notify("第二条消息") # 此时观察者已不在列表中
print("测试结束")
test()
输出结果:
观察者A 收到: 第一条消息
删除观察者...
观察者A 被销毁了
第二条消息
测试结束
看到了吗?del obs 之后,观察者对象立即被销毁了。第二次 notify 时,已经没有观察者了。这就是弱引用的威力——主题不再“绑架”观察者的生命周期。
用 SVG 图理清关系
下面这张图展示了强引用和弱引用的区别:
还有哪些坑?我踩过的几个
用 weakref 也不是银弹。我分享几个实战中遇到的坑:
- 回调函数不能用弱引用:如果你把普通函数或 lambda 作为观察者,
weakref会报错。因为函数对象不是“可弱引用”的。解决办法是用weakref.WeakMethod来包装绑定方法。 - 遍历时集合可能变化:虽然
WeakSet在遍历时会跳过已死亡的对象,但如果你在遍历过程中添加或删除元素,还是可能出问题。我建议在notify时先转成列表:for obs in list(self._observers):。 - 不要依赖
__del__:我见过有人用__del__来自动解注册,但 Python 不保证__del__的调用时机。更稳妥的做法是在观察者的close()或dispose()方法中显式调用detach。
weakref.WeakKeyDictionary 来存储函数和它的弱引用代理。不过说实话,我一般直接要求观察者必须是对象实例,这样用 WeakSet 最省事。
完整示例:带自动清理的观察者
最后,我给出一个更完整的版本,结合了 WeakSet 和 WeakMethod:
import weakref
class SafeSubject:
def __init__(self):
self._observers = weakref.WeakSet()
def attach(self, observer):
# 如果 observer 是对象,直接添加
self._observers.add(observer)
def attach_method(self, obj, method):
# 如果观察者是一个绑定方法,用 WeakMethod
self._observers.add(weakref.WeakMethod(method, self._cleanup))
def _cleanup(self, ref):
# 当弱引用对象被回收时,自动清理
pass
def notify(self, message):
for observer in list(self._observers):
if isinstance(observer, weakref.WeakMethod):
method = observer()
if method:
method(message)
else:
observer.update(message)
# 使用示例
class Button:
def __init__(self, label):
self.label = label
def on_click(self, event):
print(f"{self.label} 按钮点击: {event}")
subject = SafeSubject()
btn = Button("提交")
subject.attach(btn)
subject.notify("用户点击了提交按钮")
这个版本里,WeakMethod 确保即使观察者对象被删除,也不会留下悬空引用。不过说实话,日常开发中 WeakSet 已经够用了,WeakMethod 属于锦上添花。
总结一下
观察者模式的内存泄漏,说白了就是主题活得太久,观察者死得太晚。用 weakref 解决这个问题,是 Python 社区的标准做法。我个人建议:
- 优先用
weakref.WeakSet管理观察者集合 - 如果观察者是绑定方法,考虑
weakref.WeakMethod - 永远不要假设观察者会主动解注册
- 在
notify时做一次快照(转成 list),避免遍历时集合变动
嗯,这一节的内容就到这里。代码不多,但背后的原理值得你多琢磨几遍。下次遇到观察者模式,记得问自己一句:我的观察者,真的能被回收吗?
公众号:蓝海资料掘金营,微信deep3321