# python weakref 循环引用
python weakref
1. 内存泄漏
前面已经熟悉了python内存回收机制:引用计数,分代回收。
但是,仍有一个问题:无法回收循环引用对象。
只有容器对象才会形成循环引用,比如list、class、deque、dict、set等都属于容器类型。
import gc
class A(object):
def __init__(self):
self.data = [x for x in range(100000)]
self.child = None
def __del__(self):
pass
def cycle_ref():
a1 = A()
a2 = A()
a1.child = a2
a2.child = a1
if __name__ == '__main__':
import time
while True:
time.sleep(0.5)
cycle_ref()
测试结果:
- 理论上a1,a2都是局部变量,应该被回收,但实际是如果使用循环引用,进程占用内存会不停飙升;
- 注释掉循环引用的两句,进程占用内存保持在11.5M。
- 结果表明,循环引用会导致内存泄漏。
网上的文档基本上表示gc无法回收循环引用的对象。
继续测试
import gc
class A(object):
def __init__(self):
self.data = [x for x in range(100000)]
self.child = None
def __del__(self):
pass
def cycle_ref():
a1 = A()
a2 = A()
a1.child = a2
a2.child = a1
if __name__ == '__main__':
import time
while True:
time.sleep(0.5)
cycle_ref()
gc.collect()
print(gc.garbage)
测试结果(python 3.7.7, win8, idle):
- 手动gc.collect()可以回收循环引用对象,内存占用保持在12M左右;
- del方法并不会影响回收。部分文档声称类自定义del方法会导致gc无法回收循环引用对象,实测结果并不支持这一点;
- gc.garbage输出仍保持空。
1.1. 解决方法
解决方法有两种:手动/自动。
手动很简单,在不使用时解除引用。
def cycle_ref():
a1 = A()
a2 = A()
a1.child = a2
a2.child = a1
# 解除循环引用,避免内存泄露
a1.child = None
a2.child = None
当然,这比较考验开发的功底,也容易出错,weakref就是自动化的解决方案。
2. weakref
Python标准库提供了weakref模块,弱引用不会在引用计数中计数,其主要目的是解决循环引用。并非所有的对象都支持weakref,例如list和dict就不支持。下面是weakref比较常用的方法:
- class weakref.ref(object[, callback]) :创建一个弱引用对象,object是被引用的对象,callback是回调函数(当被引用对象被删除时,调用该回调函数);
- weakref.proxy(object[, callback]):创建一个用弱引用实现的代理对象,参数同上;
- weakref.getweakrefcount(object) :获取对象object关联的弱引用对象数;
- weakref.getweakrefs(object):获取object关联的弱引用对象列表;
- class weakref.WeakKeyDictionary([dict]):创建key为弱引用对象的字典;
- class weakref.WeakValueDictionary([dict]):创建value为弱引用对象的字典;
- class weakref.WeakSet([elements]):创建成员为弱引用对象的集合对象。
使用r_obj = weakref.ref(obj)创建的对象引用时需要使用r_obj()形式调用,所以常用做法是使用r_obj = weakref.proxy(obj);这样调用时可以直接使用r_obj。
2.1. 使用案例
对上面的案例代码改造一下便可解决内存泄漏问题:
import gc
import weakref
class A(object):
def __init__(self):
self.data = [x for x in range(100000)]
self.child = None
def __del__(self):
pass
def cycle_ref():
a1 = A()
a2 = A()
a1.child = weakref.proxy(a2)
a2.child = weakref.proxy(a1)
if __name__ == '__main__':
import time
while True:
time.sleep(0.5)
cycle_ref()
#gc.collect()
print(gc.garbage)
3. 问题
下面的代码中gc.garbage总是为空,与文档所描述的情形不符。
可以观察到删除循环引用的两个对象时不一定会立即回收,需要手动调用gc.collect(0、1、2)才能回收。
但gc.garbage始终为空。
class A():
def __del__(self):
print('del A')
class B():
def __del__(self):
print('del B')
a = A()
b = B()
a._n = b
b._n = a
import gc
class Node(object):
def __init__(self, data):
self.data = data
self.parent = None
self.children = []
def add_child(self, child):
self.children.append(child)
child.parent = self
def __del__(self):
print('__del__')
n = Node(0)
del n
# __del__
n1 = Node(1)
n2 = Node(2)
n1.add_child(n2)
del n1 # no output
n2.parent
del n2
4. 总结
- 在3.7.7中gc可以回收循环引用对象;
- 但既然weakref存在,并且很多模块也在使用它,就目前的情况而言,在开发时使用weakref而不是依赖于gc是一件惠而不费的事情 。
日拱一卒无有尽,功不唐捐终入海

浙公网安备 33010602011771号