news 2026/9/3 16:18:33

只改一个方向的引用,循环引用就能立刻被回收?一文讲透 weakref 弱引用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
只改一个方向的引用,循环引用就能立刻被回收?一文讲透 weakref 弱引用

「Python 进阶之路」系列 Day23

写在前面

Day22 讲了循环引用要靠gc.collect()兜底才能清理,del完对象却"卡"在内存里不动。今天这篇往前一步:能不能从设计上直接避免循环引用产生,而不是每次都指望 GC 事后来擦屁股?答案是weakref——一种不增加引用计数的引用方式。


一、是什么:弱引用不增加引用计数

弱引用(weak reference):一种"不计入引用计数"的引用方式。用weakref.ref(obj)创建一个弱引用对象,通过调用它(r())可以拿到原对象;但这个弱引用本身不会让原对象的引用计数加一——如果原对象的其他强引用都没了,弱引用完全不能阻止它被回收。

importweakref,sysclassNode:def__init__(self,name):self.name=namedef__del__(self):print(f"Node{self.name}被销毁了")n=Node("A")print(sys.getrefcount(n))# 2r=weakref.ref(n)# 创建弱引用print(sys.getrefcount(n))# 2 —— 完全没变!弱引用不增加引用计数print(r())# <__main__.Node object at 0x...>print(r()isn)# True —— 弱引用调用能拿到原对象

对象被销毁后,弱引用调用会返回None

deln# 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.name=name self.other=Nonedef__del__(self):print(f"NodeWeak{self.name}被销毁了")a=NodeWeak("a")b=NodeWeak("b")a.other=b# a 强引用 bb.other=weakref.ref(a)# b 只弱引用 a,不增加a的引用计数deladelb# NodeWeak a 被销毁了# NodeWeak b 被销毁了# ↑ del 完立刻触发销毁,完全不需要 gc.collect() 介入!

强引用

弱引用不计入引用计数

NodeA

NodeB

对比 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.value=valuedef__del__(self):print("Data 被销毁了")d=Data(42)p=weakref.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.name=namedef__del__(self):print(f"Resource{self.name}被回收了")cache=weakref.WeakValueDictionary()res=Resource("res1")cache["key1"]=resprint("key1"incache)# Trueprint(cache["key1"].name)# res1delres# 删除唯一的强引用# Resource res1 被回收了 ← 立刻被回收,缓存没能"续命"print("key1"incache)# False —— 对象没了,缓存里的条目也自动消失

如果这里用的是普通dict做缓存,cache["key1"] = res会给res增加一次强引用,哪怕外部的res变量被删了,缓存里那份引用也会让对象一直存活,变成事实上的内存泄漏——WeakValueDictionary恰好避免了这个问题,值被外部回收时会自动从字典里消失。


四、面试追问

Q1:gc 模块是怎么检测出循环引用的?

gc 只追踪"容器类型"对象(list、dict、自定义对象等可能参与循环的类型),不追踪 int、str 这类不可变原子类型。扫描时会计算每个被追踪对象"来自其他被追踪对象内部的引用数",从它的真实引用计数里减掉这部分:剩下计数仍大于 0,说明有外部真实引用撑着,是存活对象;剩下为 0,说明所有引用都来自彼此之间,是纯循环垃圾,可以回收。

Q2:weakref 弱引用和普通引用的区别是什么?

普通(强)引用会让对象的引用计数加一,只要强引用存在,对象就不会被回收;弱引用不会增加引用计数,通过weakref.ref(obj)()访问对象,如果对象已经因为没有强引用而被销毁,弱引用调用会返回None

Q3:为什么要主动用 weakref,而不是完全依赖 gc 兜底?

GC 扫描本身有开销,对象越多扫描成本越高;而且 GC 的回收时机不确定,不像引用计数那样立刻生效。更重要的是,某些关系本来就该是单向的"知道对方存在"而不是双向"互相拥有"(比如缓存、观察者模式、树结构里子节点指向父节点),用弱引用能更准确地表达这种语义,从设计上直接避免不必要的循环引用,而不是造出问题再指望 GC 事后清理。

Q4:weakref 有哪些常见应用场景?

常见场景包括:缓存(WeakValueDictionary,缓存的值被外部正常回收时会自动从缓存里消失,避免缓存变相阻止对象被回收)、观察者模式(被观察者用弱引用持有观察者列表,避免观察者因为被观察者持有的引用而"续命"、无法正常释放)、树/图结构里子节点指向父节点的反向引用(防止父子之间形成循环引用)。

Q5:把循环引用的例子改成一个方向用 weakref,还会不会造成"内存卡住"的问题?

不会。只要循环中有一个方向换成弱引用,这个引用就不计入引用计数,原本的循环就被打破,变成一条能正常靠引用计数回收的单向链,del完成之后对象会立刻被销毁,不再需要等待gc.collect()介入。


下一篇预告

Day24 讲 Python 性能优化实战——用cProfile定位代码里真正的性能瓶颈在哪,而不是凭直觉猜哪里慢。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 16:14:48

2026最新网络安全零基础入门教程:7天速成就业指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 16:10:23

SnapGene 8.0.1 分子克隆设计软件:核心功能、安装与合规使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 16:07:30

2026届毕业生降AI指南!5款工具实测,一次过知网维普

知网AIGC检测2026最新攻略&#xff01;亲测有效&#xff0c;AI率从70%压到9% 自从2026年知网AIGC检测系统全面迭代升级&#xff0c;全国高校几乎统一把AI写作率合格线卡死在15%以内&#xff0c;身边同学因为AI率超标被打回重改、延迟答辩的比比皆是。 这段时间我试遍了全网所…

作者头像 李华
网站建设 2026/9/3 16:06:10

ROS2多车协同仿真:从TF2坐标变换到Gazebo集群部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 16:03:10

【单片机课设毕设项目】基于 STM32 的医用输液智能调控与移动端数据可视化系统 基于 STM32 的生理体征与输液工况联合监测控制系统设计(013806)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华