只改一个方向的引用,循环引用就能立刻被回收?一文讲透 weakref 弱引用 「Python 进阶之路」系列 Day23写在前面Day22 讲了循环引用要靠gc.collect()兜底才能清理del完对象却卡在内存里不动。今天这篇往前一步能不能从设计上直接避免循环引用产生而不是每次都指望 GC 事后来擦屁股答案是weakref——一种不增加引用计数的引用方式。一、是什么弱引用不增加引用计数弱引用weak reference一种不计入引用计数的引用方式。用weakref.ref(obj)创建一个弱引用对象通过调用它r()可以拿到原对象但这个弱引用本身不会让原对象的引用计数加一——如果原对象的其他强引用都没了弱引用完全不能阻止它被回收。importweakref,sysclassNode:def__init__(self,name):self.namenamedef__del__(self):print(fNode{self.name}被销毁了)nNode(A)print(sys.getrefcount(n))# 2rweakref.ref(n)# 创建弱引用print(sys.getrefcount(n))# 2 —— 完全没变弱引用不增加引用计数print(r())# __main__.Node object at 0x...print(r()isn)# True —— 弱引用调用能拿到原对象对象被销毁后弱引用调用会返回Nonedeln# Node A 被销毁了 ← del n 立刻触发销毁因为n本来就没有循环引用print(r())# None —— 原对象已经没了弱引用调用返回None二、为什么要主动用weakref而不是完全依赖gc兜底Day22 讲了循环引用会一直卡在内存里直到gc.collect()扫描到才会被清理。既然有 GC 兜底为什么还要主动用weakref去避免循环引用GC 扫描本身有开销对象数量越多扫描一遍的成本越高能从设计上直接避免循环引用产生比依赖事后清理更高效回收时机不可控GC 什么时候扫描到、什么时候真正清理取决于分代阈值触发的时机不像引用计数那样立刻生效如果程序对内存释放的时机比较敏感这种不确定性是个问题某些场景本来就不该是双向强引用比如缓存持有对象、观察者模式里被观察者持有观察者列表、树结构里子节点指向父节点——这些场景的语义本来就是单向真正拥有、另一个方向只是知道对方存在用弱引用能更准确地表达这种关系而不是造出一个本不该存在的循环引用再指望 GC 帮忙擦屁股三、怎么用1. 实测weakref打破循环引用把 Day22 那个循环引用的例子改一下让其中一个方向变成弱引用classNodeWeak:def__init__(self,name):self.namename self.otherNonedef__del__(self):print(fNodeWeak{self.name}被销毁了)aNodeWeak(a)bNodeWeak(b)a.otherb# a 强引用 bb.otherweakref.ref(a)# b 只弱引用 a不增加a的引用计数deladelb# NodeWeak a 被销毁了# NodeWeak b 被销毁了# ↑ del 完立刻触发销毁完全不需要 gc.collect() 介入强引用弱引用不计入引用计数NodeANodeB对比 Day22 的例子双向都是强引用del之后必须靠gc.collect()才能清理这里只要有一个方向换成弱引用循环就被打破了——a的引用计数只剩b.other这个弱引用不计数del a之后a的引用计数正常归零立刻销毁a销毁后b也不再被任何东西引用跟着立刻销毁。一个方向的弱引用就足以让原本的循环变成一条能正常靠引用计数回收的单向链。2. weakref.proxy()像代理一样直接用weakref.ref()每次要拿对象都得多写一层调用r()weakref.proxy()提供了一个可以直接当原对象用的代理classData:def__init__(self,value):self.valuevaluedef__del__(self):print(Data 被销毁了)dData(42)pweakref.proxy(d)print(p.value)# 42 —— 直接像用d一样用p不用加括号调用deld# Data 被销毁了print(p.value)# ReferenceError: weakly-referenced object no longer exists原对象被销毁后再访问proxy会直接抛出ReferenceError提醒你这个对象已经不在了而不是像弱引用调用那样悄悄返回None——两种方式适合不同场景proxy更适合用起来要和原对象一模一样、但访问失效时希望立刻报错的场景。3. WeakValueDictionary不阻止对象被回收的缓存一个经典应用场景写缓存时希望缓存能加速访问但不应该因为缓存持有着对象就阻止这个对象被正常回收weakref.WeakValueDictionary就是为这个场景设计的classResource:def__init__(self,name):self.namenamedef__del__(self):print(fResource{self.name}被回收了)cacheweakref.WeakValueDictionary()resResource(res1)cache[key1]resprint(key1incache)# Trueprint(cache[key1].name)# res1delres# 删除唯一的强引用# Resource res1 被回收了 ← 立刻被回收缓存没能续命print(key1incache)# False —— 对象没了缓存里的条目也自动消失如果这里用的是普通dict做缓存cache[key1] res会给res增加一次强引用哪怕外部的res变量被删了缓存里那份引用也会让对象一直存活变成事实上的内存泄漏——WeakValueDictionary恰好避免了这个问题值被外部回收时会自动从字典里消失。四、面试追问Q1gc 模块是怎么检测出循环引用的gc 只追踪容器类型对象list、dict、自定义对象等可能参与循环的类型不追踪 int、str 这类不可变原子类型。扫描时会计算每个被追踪对象来自其他被追踪对象内部的引用数从它的真实引用计数里减掉这部分剩下计数仍大于 0说明有外部真实引用撑着是存活对象剩下为 0说明所有引用都来自彼此之间是纯循环垃圾可以回收。Q2weakref 弱引用和普通引用的区别是什么普通强引用会让对象的引用计数加一只要强引用存在对象就不会被回收弱引用不会增加引用计数通过weakref.ref(obj)()访问对象如果对象已经因为没有强引用而被销毁弱引用调用会返回None。Q3为什么要主动用 weakref而不是完全依赖 gc 兜底GC 扫描本身有开销对象越多扫描成本越高而且 GC 的回收时机不确定不像引用计数那样立刻生效。更重要的是某些关系本来就该是单向的知道对方存在而不是双向互相拥有比如缓存、观察者模式、树结构里子节点指向父节点用弱引用能更准确地表达这种语义从设计上直接避免不必要的循环引用而不是造出问题再指望 GC 事后清理。Q4weakref 有哪些常见应用场景常见场景包括缓存WeakValueDictionary缓存的值被外部正常回收时会自动从缓存里消失避免缓存变相阻止对象被回收、观察者模式被观察者用弱引用持有观察者列表避免观察者因为被观察者持有的引用而续命、无法正常释放、树/图结构里子节点指向父节点的反向引用防止父子之间形成循环引用。Q5把循环引用的例子改成一个方向用 weakref还会不会造成内存卡住的问题不会。只要循环中有一个方向换成弱引用这个引用就不计入引用计数原本的循环就被打破变成一条能正常靠引用计数回收的单向链del完成之后对象会立刻被销毁不再需要等待gc.collect()介入。下一篇预告Day24 讲 Python 性能优化实战——用cProfile定位代码里真正的性能瓶颈在哪而不是凭直觉猜哪里慢。