Python weakref 实战:用弱引用打破循环、做自动失效缓存与观察者
Python weakref 实战:用弱引用打破循环、做自动失效缓存与观察者你写了个缓存,key 是对象 id,value 是算好的结果。跑了一天发现内存只涨不降——那些早该被回收的对象,因为还被你的缓存字典「拉着」,一个都没释放。或者你做了个父子节点互相引用的树,__del__死活不触发。这两个坑的解药都是同一个冷门模块:weakref。这篇讲清楚弱引用到底弱在哪、三个真实用途,以及那个「引用还在但对象没了」的经典陷阱。先理解「强引用」为什么会漏Python 靠引用计数回收对象:只要还有一个普通变量指向它,它就不会被释放。你的缓存字典就是一个强引用:cache{}classImage:def__init__(self,path):self.pathpath self.databx*10_000_000# 假装很占内存defload(path):imgImage(path)cache[id(img)]img# 缓存持有强引用returnimg imgload(a.png)delimg# 你以为它没了# 但 cache 里还拉着它,10MB 永远不释放问题的本质:缓存本不该延长对象寿命。对象在别处都没人用了,缓存里那份就该跟着失效。可强引用做不到——它反而把对象钉死在内存里。weakref.ref:一个不增加引用计数的引用弱引用指向对象,但不阻止它被回收。对象一旦没有强引用了,GC 照常把它收走,此时弱引用「解引用」会拿到None:importweakrefclassNode:def__init__(self,name):self.namename nNode(root)rweakref.ref(n)# 弱引用print(r())# Node ... 对象还活着,调用弱引用拿到它deln# 唯一的强引用没了print(r())# None —— 对象已被回收,弱引用自动失效注意语法:weakref.ref返回的是个可调用对象,要r()才拿到目标,拿到None说明目标已死。这个「先判空再用」是弱引用的基本纪律。用途一:自动失效的缓存(WeakValueDictionary)手动管理弱引用很啰嗦,标准库给了现成的字典。WeakValueDictionary的value 是弱引用:当某个 value 对象在别处没人引用了,它会自动从字典里消失。importweakrefclassImage:def__init__(self,path):self.pathpath _cacheweakref.WeakValueDictionary()defload(path):img_cache.get(path)ifimgisNone:imgImage(path)# 只有这里是新建_cache[path]img# 存的是弱引用returnimg aload(x.png)bload(x.png)print(aisb)# True,命中缓存,复用同一个对象print(len(_cache))# 1dela,b# 没有其他强引用了importgc;gc.collect()print(len(_cache))# 0 —— 对象被回收,缓存项自动清掉这正是我们想要的语义:只要外面还有人用这个对象,缓存就复用它;一旦没人用,缓存自己清干净,内存不会越堆越高。对应地还有WeakKeyDictionary(key 是弱引用),适合「给某个对象挂附加数据、对象没了附加数据也自动清」。用途二:打破循环引用,让del能触发父子互相引用会形成循环。虽然 CPython 有循环垃圾回收器兜底,但循环里只要有对象定义了__del__,历史版本可能回收不及时,而且靠 GC 兜底本身就不干净。让其中一个方向用弱引用,循环就断了:importweakrefclassParent:def__init__(self):self.children[]defadd(self,child):self.children.append(child)# 父 → 子:强引用child._parentweakref.ref(self)# 子 → 父:弱引用,不成环classChild:def__init__(self):self._parentNonepropertydefparent(self):returnself._parent()ifself._parentelseNone# 解引用要判空pParent()cChild()p.add(c)print(c.parentisp)# Truedelp# 父的唯一强引用没了print(c.parent)# None —— 父已回收,子不再拉着它规则:「拥有」的方向用强引用(父拥有子),「反向指回」的方向用弱引用(子指向父)。这样删掉父节点,整棵子树能干净释放,而不是父子互相拉着谁都走不了。用途三:观察者/回调不泄漏(WeakMethod)发布-订阅里,subject 持有一堆 observer 的回调。如果用强引用,observer 明明该销毁了,却因为 subject 还拉着它的方法而活着——典型的「lapsed listener」泄漏。weakref.WeakMethod专治绑定方法的弱引用(普通weakref.ref对obj.method会立刻失效,因为绑定方法是临时对象):importweakrefclassSubject:def__init__(self):self._observers[]defsubscribe(self,callback):# 绑定方法要用 WeakMethod,普通 ref 存不住self._observers.append(weakref.WeakMethod(callback))defnotify(self,event):alive[]forrefinself._observers:cbref()# 取回真正的方法ifcbisnotNone:# observer 还活着才回调cb(event)alive.append(ref)self._observersalive# 顺手清掉已死的订阅者classLogger:defon_event(self,e):print(log:,e)sSubject()logLogger()s.subscribe(log.on_event)s.notify(hi)# log: hidellog# 订阅者销毁s.notify(bye)# 没有输出,死掉的订阅自动跳过并清理subject 不再因为「订阅了」就把 observer 钉死在内存里,observer 的生命周期由它自己的持有者说了算。那个必踩的陷阱:先赋值给变量再用弱引用最隐蔽的 bug:你以为对象活着,解引用的瞬间它没了。看这段:# 危险:两次解引用之间对象可能被回收ifr()isnotNone:r().do_something()# 极端情况下这里 r() 已变 None → AttributeError正确做法是先解引用到一个局部变量,这个局部变量是强引用,能保证接下来的代码块里对象不被回收:objr()# 先抓成强引用ifobjisnotNone:obj.do_something()# 这段期间 obj 一定活着# 用完 obj 出作用域,强引用释放,对象照常可回收记住:弱引用只能用来「看一眼」,真要连续操作,先落成局部强引用。小结强引用会延长对象寿命,拿来做缓存/反向引用/回调列表容易内存泄漏;弱引用指向对象但不阻止回收,对象没了自动变None。三个即插即用工具:WeakValueDictionary做自动失效缓存、子指父用weakref.ref打破循环引用、WeakMethod做不泄漏的观察者回调。铁律一:解引用r()后永远先判空;铁律二:要连续操作就先落成局部强引用,别在一个表达式里反复r()。记住「拥有用强、指回用弱」,你的对象图就能干净地被回收。一句话记忆:弱引用是「不占有的旁观者」——它让缓存和回调按对象真实寿命自动清理,而不是把对象钉死在内存里。