# 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()  

测试结果:

  1. 理论上a1,a2都是局部变量,应该被回收,但实际是如果使用循环引用,进程占用内存会不停飙升;
  2. 注释掉循环引用的两句,进程占用内存保持在11.5M。
  3. 结果表明,循环引用会导致内存泄漏。

网上的文档基本上表示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):

  1. 手动gc.collect()可以回收循环引用对象,内存占用保持在12M左右;
  2. del方法并不会影响回收。部分文档声称类自定义del方法会导致gc无法回收循环引用对象,实测结果并不支持这一点;
  3. 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比较常用的方法:

  1. class weakref.ref(object[, callback]) :创建一个弱引用对象,object是被引用的对象,callback是回调函数(当被引用对象被删除时,调用该回调函数);
  2. weakref.proxy(object[, callback]):创建一个用弱引用实现的代理对象,参数同上;
  3. weakref.getweakrefcount(object) :获取对象object关联的弱引用对象数;
  4. weakref.getweakrefs(object):获取object关联的弱引用对象列表;
  5. class weakref.WeakKeyDictionary([dict]):创建key为弱引用对象的字典;
  6. class weakref.WeakValueDictionary([dict]):创建value为弱引用对象的字典;
  7. 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. 总结

  1. 在3.7.7中gc可以回收循环引用对象;
  2. 但既然weakref存在,并且很多模块也在使用它,就目前的情况而言,在开发时使用weakref而不是依赖于gc是一件惠而不费的事情 。
posted @ 2020-05-26 09:15  木林森__𣛧  阅读(412)  评论(0)    收藏  举报