一句话结论 :弱引用是「能拿到对象、但不给对象续命」的一张便条。weakref.ref(obj) 不会让引用计数加一,所以对象一旦没有强引用,ref() 立刻返回 None;它解决的不是「访问」,而是「观察一个对象什么时候被回收」,以及「做缓存却不想把缓存本身变成内存泄漏」。
本文所有数字均来自本机 CPython 3.13.14 实测,代码可直接复制运行。
一、先看引用计数:弱引用到底「弱」在哪
python
import sys, weakref, gc
class Node:
def __init__(self, name): self.name = name
n = Node('a')
print(sys.getrefcount(n)) # 2
r = weakref.ref(n)
print(sys.getrefcount(n)) # 2 ← 没变
print(r() is n) # True
实测输出 2 → 2。sys.getrefcount 本身会因参数传递临时 +1,所以这里的 2 就是「变量 n」+「getrefcount 的临时引用」。
关键对比:如果写成 alias = n,计数会变成 3。而 weakref.ref(n) 之后仍是 2 ------ 弱引用不参与引用计数,这是「弱」的全部含义。
于是:
python
del n
gc.collect()
print(r()) # None
对象被回收,r() 返回 None,而不是抛异常。这是 ref 与 proxy 最大的行为差异。
二、回调:注意拿到的是一个「已经死了」的引用
python
def cb(ref):
print('callback fired:', ref)
n = Node('a')
r = weakref.ref(n, cb)
del n
gc.collect()
实测输出:
css
callback fired: <weakref at 0x000001F16F58B060; dead>
这里有个很容易踩的坑:回调被调用时对象已经被回收了,参数 ref 是 dead 状态,ref() 返回 None。想在回调里读对象的属性、打日志带上对象名,是拿不到的。正确做法是把需要的信息提前闭包捕获:
python
def make_cb(name):
def cb(ref):
print(f'{name} 被回收了')
return cb
r = weakref.ref(n, make_cb(n.name))
另外回调是在回收过程中同步调用的,别在里面做重活或再创建对已死对象的强引用,也别假设此时 GC 已经跑完。
三、proxy:像用原对象一样用,死了就抛异常
python
p = Node('p')
pr = weakref.proxy(p)
print(pr.name) # p
del p; gc.collect()
print(pr.name) # ReferenceError
实测抛:
perl
ReferenceError: weakly-referenced object no longer exists
| 形态 | 对象还活着 | 对象已回收 | 适用场景 |
|---|---|---|---|
weakref.ref(o) |
r() 返回对象 |
r() 返回 None |
需要显式判空,最常用 |
weakref.proxy(o) |
直接 pr.x |
抛 ReferenceError |
想让调用方无感,但必须能接受异常 |
proxy 还有个细节:type(pr) 是 ProxyType 而不是原类,isinstance 判断会失效。业务代码里我一般只用 ref,因为它把「可能没有」这件事显式暴露出来了。
四、不是所有对象都能被弱引用
实测六类内置对象:
| 类型 | weakref.ref() |
原因 |
|---|---|---|
int |
❌ TypeError | 不可变且常被缓存/小整数池常驻 |
str |
❌ TypeError | 不可变,可能驻留 |
tuple |
❌ TypeError | 不可变容器 |
list |
❌ TypeError | 未实现 __weakref__ |
dict |
❌ TypeError | 未实现 __weakref__ |
frozenset |
✅ OK | 例外:支持弱引用 |
报错文案统一是 cannot create weak reference to 'int' object 这类。
值得单独说 frozenset:它是不可变容器,但实测 可以 弱引用。所以「不可变就不能弱引用」不是准确规律,准确说法是 类型定义里是否带 __weakref__ 槽位。
五、__slots__ 的坑:省了内存,丢了弱引用
python
class Slot:
__slots__ = ('x',)
def __init__(self): self.x = 1
weakref.ref(Slot()) # TypeError
实测报 cannot create weak reference to 'Slot' object。因为 __slots__ 明确列出了所有实例属性,默认不含 __weakref__。修法只有一个:
python
class Slot:
__slots__ = ('x', '__weakref__')
用 __slots__ 做内存优化、又同时要做缓存/观察者模式时,这一条几乎必踩。
六、WeakValueDictionary:缓存自动清理
python
class Big:
def __init__(self, k): self.k = k
wvd = weakref.WeakValueDictionary()
b = Big(1)
wvd['one'] = b
print(len(wvd), wvd.get('one')) # 1 <Big object>
del b; gc.collect()
print(len(wvd), wvd.get('one')) # 0 None
实测 len 从 1 自动变 0,get 返回 None,不需要手动清理。
这是弱引用最实用的场景:图片/大对象缓存。用普通 dict 存,缓存本身会让对象永远不被回收,缓存越大内存越涨;换成 WeakValueDictionary,业务侧没有强引用时条目自动消失。
对应的还有 WeakKeyDictionary(键弱引用,适合给对象挂元数据而不延长其生命)和 WeakSet。
⚠️ 注意 WeakValueDictionary 的值是弱引用,键仍是强引用 ,且值必须可弱引用。存 int/str 结果会直接 TypeError。
七、性能:解引用一次多少钱
本机 10 万次实测:
python
timeit.timeit("x=n.name", setup="...", number=100000) # 0.0022 s
timeit.timeit("x=r()", setup="...", number=100000) # 0.0049 s
解引用约是普通属性读取的 2.2 倍 (0.0049 / 0.0022)。单次开销在几十纳秒量级,绝大多数场景可忽略;但在热循环里对同一对象反复 r(),不如先取一次:
python
obj = r()
if obj is not None:
for _ in range(10000):
obj.work() # 别在循环里反复 r()
八、六条排错清单
r()突然返回None→ 不是 bug,是有强引用被删了。检查是否只把对象存在了WeakValueDictionary里。TypeError: cannot create weak reference→ 对象是内置不可变类型,或用了__slots__没加__weakref__。给自定义类加__slots__ = (..., '__weakref__')。- 回调里读不到对象属性 → 回调触发时对象已死,把信息提前闭包捕获。
proxy抛ReferenceError→ 预期行为,改用ref()+ 判空,或捕获异常。WeakValueDictionary条目莫名消失 → 检查是否有 GC(gc.collect())或临时变量出作用域;CPython 引用计数是即时回收,不是「等一会儿」。- 循环引用对象一直不释放 → 弱引用不打破循环,
A.b = B; B.a = A仍需gc分代回收;弱引用只是让你能「观察」到它。
九、完整可运行示例
python
import sys, weakref, gc
class Node:
def __init__(self, name): self.name = name
def __repr__(self): return f'Node({self.name})'
def demo():
n = Node('a')
print('refcount after ref:', end=' ')
base = sys.getrefcount(n)
r = weakref.ref(n)
print(sys.getrefcount(n), '(unchanged)' if sys.getrefcount(n) == base else '(changed)')
cache = weakref.WeakValueDictionary()
cache['a'] = n
print('cache alive:', len(cache), cache.get('a'))
del n
gc.collect()
print('after del -> r():', r(), '| cache:', len(cache), cache.get('a'))
p = Node('p'); pr = weakref.proxy(p)
del p; gc.collect()
try:
pr.name
except ReferenceError as e:
print('proxy ->', e)
demo()
十、什么时候该用,什么时候别用
该用 :大对象缓存、观察者/监听器注册表、给对象挂元数据(WeakKeyDictionary)、调试内存泄漏时观察生命周期。
别用:需要保证对象一定存在的业务路径(用强引用 + 显式生命周期管理);拿弱引用当「延迟加载」的替代品;在回调里做关键业务。
弱引用不是「更轻的引用」,它是不参与所有权的一张便条 。想明白这一点,前面所有的 None、ReferenceError、自动消失的缓存条目,就都不奇怪了。
实测环境:CPython 3.13.14(Windows)。所有输出为本机 d56-probe.py 实际运行结果,未做修饰。