ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python weakref 实战:用弱引用打破循环、做自动失效缓存与观察者

Python weakref 实战:用弱引用打破循环、做自动失效缓存与观察者

Python weakref 实战:用弱引用打破循环、做自动失效缓存与观察者

你写了个缓存,key 是对象 id,value 是算好的结果。跑了一天发现内存只涨不降——那些早该被回收的对象,因为还被你的缓存字典「拉着」,一个都没释放。或者你做了个父子节点互相引用的树,__del__死活不触发。这两个坑的解药都是同一个冷门模块:weakref。这篇讲清楚弱引用到底弱在哪、三个真实用途,以及那个「引用还在但对象没了」的经典陷阱。

先理解「强引用」为什么会漏

Python 靠引用计数回收对象:只要还有一个普通变量指向它,它就不会被释放。你的缓存字典就是一个强引用:

cache={}classImage:def__init__(self,path):self.path=path self.data=b"x"*10_000_000# 假装很占内存defload(path):img=Image(path)cache[id(img)]=img# 缓存持有强引用returnimg img=load("a.png")delimg# 你以为它没了# 但 cache 里还拉着它,10MB 永远不释放

问题的本质:缓存本不该延长对象寿命。对象在别处都没人用了,缓存里那份就该跟着失效。可强引用做不到——它反而把对象钉死在内存里。

weakref.ref:一个不增加引用计数的引用

弱引用指向对象,但不阻止它被回收。对象一旦没有强引用了,GC 照常把它收走,此时弱引用「解引用」会拿到None:

importweakrefclassNode:def__init__(self,name):self.name=name n=Node("root")r=weakref.ref(n)# 弱引用print(r())# <Node ...> 对象还活着,调用弱引用拿到它deln# 唯一的强引用没了print(r())# None —— 对象已被回收,弱引用自动失效

注意语法:weakref.ref返回的是个可调用对象,要r()才拿到目标,拿到None说明目标已死。这个「先判空再用」是弱引用的基本纪律。

用途一:自动失效的缓存(WeakValueDictionary)

手动管理弱引用很啰嗦,标准库给了现成的字典。WeakValueDictionaryvalue 是弱引用:当某个 value 对象在别处没人引用了,它会自动从字典里消失

importweakrefclassImage:def__init__(self,path):self.path=path _cache=weakref.WeakValueDictionary()defload(path):img=_cache.get(path)ifimgisNone:img=Image(path)# 只有这里是新建_cache[path]=img# 存的是弱引用returnimg a=load("x.png")b=load("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._parent=weakref.ref(self)# 子 → 父:弱引用,不成环classChild:def__init__(self):self._parent=None@propertydefparent(self):returnself._parent()ifself._parentelseNone# 解引用要判空p=Parent()c=Child()p.add(c)print(c.parentisp)# Truedelp# 父的唯一强引用没了print(c.parent)# None —— 父已回收,子不再拉着它

规则:「拥有」的方向用强引用(父拥有子),「反向指回」的方向用弱引用(子指向父)。这样删掉父节点,整棵子树能干净释放,而不是父子互相拉着谁都走不了。

用途三:观察者/回调不泄漏(WeakMethod)

发布-订阅里,subject 持有一堆 observer 的回调。如果用强引用,observer 明明该销毁了,却因为 subject 还拉着它的方法而活着——典型的「lapsed listener」泄漏。weakref.WeakMethod专治绑定方法的弱引用(普通weakref.refobj.method会立刻失效,因为绑定方法是临时对象):

importweakrefclassSubject:def__init__(self):self._observers=[]defsubscribe(self,callback):# 绑定方法要用 WeakMethod,普通 ref 存不住self._observers.append(weakref.WeakMethod(callback))defnotify(self,event):alive=[]forrefinself._observers:cb=ref()# 取回真正的方法ifcbisnotNone:# observer 还活着才回调cb(event)alive.append(ref)self._observers=alive# 顺手清掉已死的订阅者classLogger:defon_event(self,e):print("log:",e)s=Subject()log=Logger()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

正确做法是先解引用到一个局部变量,这个局部变量是强引用,能保证接下来的代码块里对象不被回收:

obj=r()# 先抓成强引用ifobjisnotNone:obj.do_something()# 这段期间 obj 一定活着# 用完 obj 出作用域,强引用释放,对象照常可回收

记住:弱引用只能用来「看一眼」,真要连续操作,先落成局部强引用

小结

  • 强引用会延长对象寿命,拿来做缓存/反向引用/回调列表容易内存泄漏;弱引用指向对象但不阻止回收,对象没了自动变None
  • 三个即插即用工具:WeakValueDictionary自动失效缓存、子指父用weakref.ref打破循环引用WeakMethod不泄漏的观察者回调
  • 铁律一:解引用r()永远先判空;铁律二:要连续操作就先落成局部强引用,别在一个表达式里反复r()
  • 记住「拥有用强、指回用弱」,你的对象图就能干净地被回收。

一句话记忆:弱引用是「不占有的旁观者」——它让缓存和回调按对象真实寿命自动清理,而不是把对象钉死在内存里。

返回列表