① 什么是循环引用?一句话看懂 🔄
A 持有 B,B 持有 A(或闭环),导致引用计数 ≠ 0,GC 无法回收。
类比:两人互锁手臂,谁也躺不下。
② 制造循环:10 秒手写一个 🪝
class Node:
def __init__(self, val):
self.val = val
self.next = None
a = Node(1); b = Node(2)
a.next = b; b.next = a # 闭环!
结果:
del a, b后内存仍占用,GC 只能回收无环垃圾。
③ 检测工具:objgraph 一行命令 🔍
pip install objgraph
import objgraph, gc
# 制造循环
a = []; b = []; a.append(b); b.append(a)
# 强制 GC
gc.collect()
# 可视化循环
objgraph.show_backrefs([a], filename='loop.png', max_depth=3)
生成
loop.png:红色箭头 = 回环,一目了然!
④ 手动检测:10 行纯标准库 🧪
import gc, sys
def find_loops(obj):
"""返回与 obj 形成回环的所有对象"""
gc.collect() # 强制回收无环垃圾
visited = set()
to_visit = [obj]
while to_visit:
cur = to_visit.pop()
if id(cur) in visited: continue
visited.add(id(cur))
for ref in gc.get_referents(cur):
if ref is obj: # 回到起点 → 环!
yield cur
elif id(ref) not in visited:
to_visit.append(ref)
# 使用
a = []; b = []; a.append(b); b.append(a)
loops = list(find_loops(a))
print(len(loops)) # 2
⑤ 弱引用破解:万能解毒剂 🩹
| 工具 | 作用 | 示例 |
|---|---|---|
weakref.ref | 不增加引用计数 | ref = weakref.ref(obj) |
weakref.proxy | 透明代理 | proxy = weakref.proxy(obj) |
weakref.WeakKeyDictionary | 弱键字典 | 缓存键不阻止 GC |
weakref.WeakValueDictionary | 弱值字典 | 缓存值不阻止 GC |
import weakref
class Node:
def __init__(self, val):
self.val = val
self.next = None # 普通引用 → 可能循环
# 破解:next 改成弱引用
class NodeWeak:
def __init__(self, val):
self.val = val
self._next = None
@property
def next(self):
return self._next() if self._next else None
@next.setter
def next(self, node):
self._next = weakref.ref(node) if node else None
结果:
del后立即回收,无循环!
⑥ 实战:弱引用 LRU 缓存 🍪
import weakref, functools
@functools.lru_cache(maxsize=128)
def expensive_func(x):
return x ** 2
# 缓存键/值都是弱引用 → 不阻止 GC
class WeakLRU:
def __init__(self, func, maxsize=128):
self.func = func
self.cache = weakref.WeakValueDictionary()
self.maxsize = maxsize
def __call__(self, x):
if x in self.cache: return self.cache[x]
if len(self.cache) >= self.maxsize: self.cache.popitem()
result = self.func(x)
self.cache[x] = result
return result
⑦ 彩蛋:循环引用计数器(10 行)🔢
import gc
def count_loops(obj):
gc.collect()
visited = set()
stack = [(obj, [obj])] # (current, path)
loops = 0
while stack:
cur, path = stack.pop()
if id(cur) in visited: continue
visited.add(id(cur))
for ref in gc.get_referents(cur):
if ref is path[0]: loops += 1; continue
if id(ref) not in visited:
stack.append((ref, path + [ref]))
return loops
# 使用
a = []; b = []; a.append(b); b.append(a)
print(count_loops(a)) # 2
⑧ 万能解毒剂:检查清单 ✅
| 场景 | 破解法 |
|---|---|
| 双向链表 | 用 weakref.ref 或 weakref.proxy |
| 父-子循环 | 父 → 子用弱引用 |
| 缓存循环 | 用 WeakValueDictionary |
| 观察者模式 | 用 weakref.WeakSet |
口诀:“能弱就弱,能拆就拆,能 GC 就 GC”
⑨ 彩蛋:终端可视化(15 行)🌈
import objgraph, sys, os
from PIL import Image # pip install pillow
def viz_loop(obj, out='loop.png'):
objgraph.show_backrefs([obj], filename=out, max_depth=3,
highlight=lambda x: x is obj,
extra_info=lambda x: str(type(x).__name__))
print(f"✅ 循环图已生成:{os.path.abspath(out)}")
# 使用
a = []; b = []; a.append(b); b.append(a)
viz_loop(a) # 自动生成 loop.png
🏁 一句话口诀(背它!)
**“弱引用破解,手动检测,objgraph 可视化,循环不再怕!”**🎵