为什么设置绑定方法到python对象创建循环引用?

时间:2014-10-02 09:27:20

标签: python memory-leaks

我正在使用Python 2.7,我喜欢这个困扰我的问题。

这是最简单的例子:

>>> class A(object):
    def __del__(self):
        print("DEL")
    def a(self):
        pass

>>> a = A()
>>> del a
DEL

这就像预期的那样......现在我正在尝试更改对象a()的{​​{1}}方法,发生的事情是更改后我无法删除a更多:

a

只是做一些检查我在作业之前和之后打印了>>> a = A() >>> a.a = a.a >>> del a 参考

a.a

最后,我使用>>> a = A() >>> print a.a <bound method A.a of <__main__.A object at 0xe86110>> >>> a.a = a.a >>> print a.a <bound method A.a of <__main__.A object at 0xe86110>> 模块试图理解为什么没有释放对象:

objgraph

pre-backref-graph.png

>>> b = A()
>>> import objgraph
>>> objgraph.show_backrefs([b], filename='pre-backref-graph.png')

post-backref-graph.png

正如您在>>> b.a = b.a >>> objgraph.show_backrefs([b], filename='post-backref-graph.png') 图片中看到的那样,b中有post-backref-graph.png个引用对我没有意义,因为应该忽略实例方法的自引用(就像在赋值之前一样)。

有人可以解释为什么这种行为以及我该如何解决它?

3 个答案:

答案 0 :(得分:4)

Veedrac关于保持对实例的引用的绑定方法的答案只是答案的一部分。 CPython的垃圾收集器知道如何检测和处理循环引用 - 除非某个循环的一部分对象具有__del__方法,如此处所提到的https://docs.python.org/2/library/gc.html#gc.garbage

  

具有__del__()方法且属于参考周期的对象   导致整个参考周期无法收集,包括   对象不一定在循环中,但只能从它到达。   Python不会自动收集这样的循环,因为一般情况下   Python无法猜测运行安全顺序的安全顺序   __del__()方法。 (...)通常不要通过不创建包含__del__()方法的对象的循环来避免这个问题   在这种情况下可以检查垃圾,以验证没有这样的循环   被创造。

IOW:删除你的__del__方法,你应该没问题。

编辑:wrt /你的评论:

  

我在对象上使用它作为函数a.a = functor(a.a)。当测试   完成后我想用原始方法替换仿函数。

然后解决方案简单明了:

a = A()
a.a = functor(a.a)
test(a)
del a.a

在你明确地绑定它之前,a没有'a'实例属性,所以它在类上查找并返回一个新的method实例(参见https://wiki.python.org/moin/FromFunctionToMethod了解更多信息这个)。然后调用此method实例,并(通常)将其丢弃。

答案 1 :(得分:3)

当你写a.a时,它会有效地运行:

A.a.__get__(a, A)

因为您没有访问预绑定方法,而是正在访问类'方法 在运行时绑定。

当你这样做时

a.a = a.a

你有效地“缓存”绑定方法的行为。由于绑定方法具有对象的引用(显然,因为它必须将self传递给函数),这将创建一个循环引用。


所以我正在为你的问题建模:

class A(object):
    def __del__(self):
        print("DEL")
    def a(self):
        pass

def log_all_calls(function):
    def inner(*args, **kwargs):
        print("Calling {}".format(function))

        try:
            return function(*args, **kwargs)
        finally:
            print("Called {}".format(function))

    return inner

a = A()
a.a = log_all_calls(a.a)

a.a()

您可以在lof_all_calls内使用弱引用按需绑定,如:

import weakref

class A(object):
    def __del__(self):
        print("DEL")
    def a(self):
        pass

def log_all_calls_weakmethod(method):
    cls = method.im_class
    func = method.im_func
    instance_ref = weakref.ref(method.im_self)
    del method

    def inner(*args, **kwargs):
        instance = instance_ref()

        if instance is None:
            raise ValueError("Cannot call weak decorator with dead instance")

        function = func.__get__(instance, cls)

        print("Calling {}".format(function))

        try:
            return function(*args, **kwargs)
        finally:
            print("Called {}".format(function))

    return inner

a = A()
a.a = log_all_calls_weakmethod(a.a)

a.a()

这真的很难看,所以我宁愿把它抽出来制作一个weakmethod装饰者:

import weakref

def weakmethod(method):
    cls = method.im_class
    func = method.im_func
    instance_ref = weakref.ref(method.im_self)
    del method

    def inner(*args, **kwargs):
        instance = instance_ref()

        if instance is None:
            raise ValueError("Cannot call weak method with dead instance")

        return func.__get__(instance, cls)(*args, **kwargs)

    return inner

class A(object):
    def __del__(self):
        print("DEL")
    def a(self):
        pass

def log_all_calls(function):
    def inner(*args, **kwargs):
        print("Calling {}".format(function))

        try:
            return function(*args, **kwargs)
        finally:
            print("Called {}".format(function))

    return inner

a = A()
a.a = log_all_calls(weakmethod(a.a))

a.a()

完成!


FWIW,Python 3.4不仅没有这些问题,而且还为您预先构建了WeakMethod

答案 2 :(得分:1)

为什么Python会这样做。技术上所有对象如果有方法则包含循环引用。但是,如果垃圾收集器必须对对象方法进行显式检查以确保释放对象不会导致问题,则垃圾收集将花费更长的时间。因此,Python将方法与对象__dict__分开存储。因此,当您编写a.a = a.a时,您将在对象的a字段中使用自身隐藏方法。因此,明确提到了防止对象被正确释放的方法。

解决问题的方法是不要费心去保持&#34;缓存&#34;原始方法,只需删除阴影变量即可。这将使方法失效,并使其再次可用。

>>> class A(object):
...     def __del__(self):
...         print("del")
...     def method(self):
...         print("method")
>>> a = A()
>>> vars(a)
{}
>>> "method" in dir(a)
True
>>> a.method = a.method
>>> vars(a)
{'method': <bound method A.method of <__main__.A object at 0x0000000001F07940>>}
>>> "method" in dir(a)
True
>>> a.method()
method
>>> del a.method
>>> vars(a)
{}
>>> "method" in dir(a)
True
>>> a.method()
method
>>> del a
del

此处vars显示对象的__dict__属性中的内容。请注意,即使__dict__有效,a.__dict__也不包含对自身的引用。 dir生成一个可从给定对象到达的所有属性的列表。在这里,我们可以看到对象的所有属性和方法,以及它的类及其基础的所有方法和属性。这表明a的绑定方法存储在与a属性存储位置分开的位置。