为什么将一个有界方法绑定到Python对象会创建循环引用?

9

我正在使用Python 2.7,发现一个令我困惑的问题。

以下是最简单的示例:

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

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

正如预期的那样,这没问题...现在我正在尝试更改对象aa()方法,但更改后,我无法再删除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模块来尝试理解为什么对象没有被释放:
>>> b = A()
>>> import objgraph
>>> objgraph.show_backrefs([b], filename='pre-backref-graph.png')

pre-backref-graph.png

>>> b.a = b.a
>>> objgraph.show_backrefs([b], filename='post-backref-graph.png')

post-backref-graph.png

从上面的图像中可以看到,在b中存在一个对于__self__的引用,但是这对于我来说没有任何意义,因为实例方法的自引用应该被忽略(就像赋值之前一样)。

有人能解释一下为什么会出现这种行为,以及我该如何解决它吗?

3个回答

5
当你写下 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()

您可以在log_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()

完成!


顺便说一下,Python 3.4不仅没有这些问题,而且还为您预先构建了WeakMethod


好的...有没有什么方法可以避免这种情况?我应该缓存一些方法并稍后恢复这些方法:这是可能的吗? - Michele d'Amico
这取决于你想要做什么。 - Veedrac
好的,我找到了解决方案:a.a = types.MethodType(A.a,a,A) - Michele d'Amico
1
Python 3.4及以上版本可以垃圾回收包含具有终结器对象的循环引用。https://www.python.org/dev/peps/pep-0442/. 它仍然是一个引用循环,但不会导致泄漏。 - Veedrac
1
@MichaelCarilli 在未来某个不确定的时间自动执行。 - Veedrac
显示剩余8条评论

4
Veedrac的答案关于绑定方法保持对实例的引用只是回答的一部分。CPython的垃圾收集器知道如何检测和处理循环引用 - 除非循环中的某个对象具有__del__方法,正如此处所述:https://docs.python.org/2/library/gc.html#gc.garbage
引用循环中有__del__()方法的对象会导致整个引用循环无法收集,包括不一定在循环中但只能从循环中到达的对象。Python不会自动收集这样的循环,因为一般来说,Python无法猜测运行__del__()方法的安全顺序。(...)通常最好避免创建包含具有__del__()方法的对象的循环,并且在该情况下可以检查垃圾以验证不会创建此类循环。
即:删除__del__方法,您应该没问题。
编辑:关于您的评论:
我将其用作函数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实例,并(通常)将其丢弃。


那不是解决方案...我需要__del__方法(原因超出范围),而且这个问题是我不知道如何解决其他弱引用工作良好的唯一问题。 - Michele d'Amico
1
好的,它实际上回答了你的问题“有人能解释一下为什么会出现这种行为,我该如何解决?” - 你没有提到你需要 __del__,而且你的代码片段也没有暗示它有任何真正的用处 ;) 现在如果你看一下我给你指的链接,那里有更多关于这个主题的内容... - bruno desthuilliers
正如您在发布的链接中所写,最好的方法是不要创建循环。我的问题是:“我不明白为什么会出现这种循环,是否有办法避免创建这种循环?”但是,删除__del__并不会消除循环,只会消除循环的副作用 :) - Michele d'Amico
好的...不错...但我想恢复原始 a.a 方法,使用它并且没有任何关于内存泄漏的问题。这就是问题所在:将一个方法分配给一个对象会创建一个循环引用,你必须通过类似于 del a.aa.a=None 的方式来打破它。 - Michele d'Amico
没有“原始的a.a方法”,参见https://wiki.python.org/moin/FromFunctionToMethod。 - bruno desthuilliers
显示剩余2条评论

1
关于Python为什么这样做。技术上讲,如果对象有方法,则所有对象都包含循环引用。然而,如果垃圾回收程序必须对一个对象的方法进行显式检查以确保释放对象不会引起问题,那么垃圾回收将会花费更长的时间。因此,Python将方法与对象的__dict__分开存储。所以当你写a.a = a.a时,在对象的a字段中用自身遮盖了该方法。因此,有一个明确的指向该方法的引用,防止对象被正确释放。
解决你的问题的方法是不需要保留原始方法的“缓存”,只需在使用完后删除遮盖变量。这样可以取消遮盖方法并使其再次可用。
>>> 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__属性中包含了什么。注意,尽管a.__dict__是有效的,但__dict__中并未包含对自身的引用。dir会列出从给定对象可访问的所有属性的列表。在这里,我们可以看到对象的所有属性和方法以及其类及其基类的所有方法和属性。这表明a的绑定方法存储在与a属性存储不同的位置。


谢谢您的回答,但真正的问题不是“为什么对象没有被销毁?”,而是为什么a.a=a.a会创建一个循环引用。如果您删除__del__覆盖并尝试在赋值之前和之后绘制图形,您会发现这两个图形是不同的,第二个图形具有循环引用。 - Michele d'Amico
误解了问题。我完全改变了我的答案,提出了新的建议,并在这个过程中学到了一些东西。 - Dunes
谢谢:这正是我所说的“为什么将绑定方法设置为Python对象会创建循环引用?”Veedrac帮我解决了一些相关问题,但是通过你的回答,我真正理解了。 - Michele d'Amico
@Dunes:“因此,Python将方法与对象的__dict__分开存储(...)这表明a的绑定方法存储在单独的位置”:更简单的是:Python根本不存储方法。它存储的是函数(在类的__dict__中),如何在查找时创建(绑定或未绑定)方法对象在这里解释:https://wiki.python.org/moin/FromFunctionToMethod - bruno desthuilliers
@brunodesthuilliers,非常有见地,谢谢。我觉得也许应该删除我的回答,因为它似乎是你的一个信息不足的副本。 - Dunes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接