在不使用临时的情况下通过__get__调用__iadd__时发生内存泄漏

时间:2013-08-17 09:41:13

标签: python memory-leaks decorator weak-references python-decorators

尝试修改装饰者而不是以使用weakref,我偶然发现了以下行为:

import weakref

class descriptor(object):
    def __get__(self, instance, owner):
        return proxy(instance)

class proxy(object):
    def __init__(self, instance):
        self.instance = instance

    def __iadd__(self, other):
        return self

class A(object):
    descr = descriptor()

def is_leaky(test_fn):
    a = A()
    wr = weakref.ref(a)
    test_fn(a)
    del a
    return wr() is not None

def test1(a):
    tmp = a.descr
    tmp += object()

def test2(a):
    a.descr += object()

print(is_leaky(test1))  # gives False
print(is_leaky(test2))  # gives True!!!

这对我来说似乎很奇怪,因为我希望两种情况都表现得一样。此外,根据我对引用计数和对象生命周期的理解,我确信在这两种情况下都应该释放对象。

我已经在python2.7和python3.3上测试了它。

这是一个错误还是故意行为? 有没有办法让两个调用都有预期的结果(释放有问题的对象)?

我不想在weakref中使用proxy,因为这会破坏绑定方法的正确对象生存期语义:

a = A()
descr = a.descr
del a   # a is kept alive since descr is a bound method to a
descr() # should execute a.descr() as expected

1 个答案:

答案 0 :(得分:5)

两个代码路径不等效。

就地添加动作两个运算符,分配目标和添加的项目。在test1 temp中,本地变量和就地添加转换为以下内容:

temp = temp.__iadd__(object())

并且由于您返回self并且temp引用了同一个对象,因此该变为temp = temp并且在函数退出后清除该引用。

test2中,你复杂的事情,因为现在描述符再次涉及:

a.descr += object() 

变为:

a.descr = A.__dict__['descr'].__get__(a, A).__iadd__(object())

所以你将A.__dict__['descr'].__get__(a, A)的结果分配给实例属性a.descr;描述符没有__set__()方法,也没有参考。

但是,这是捕获,proxy对象包含对a本身的引用,a.descr.instance是对a的引用! 您创建了一个循环引用

此引用使对象保持足够长时间以通过弱引用显示,但是一旦垃圾收集进程运行并中断此循环,a无论如何都会消失。

这个故事的道德?不要将__iadd__与非数据描述符结合使用;包括__get__ __set__,因为您需要控制在分配结果时会发生什么。