在Python中,我应该担心循环引用吗?

46

假设我有一段维护父/子结构的代码。在这样的结构中,我会遇到循环引用,其中一个子指向一个父亲,而一个父亲也指向一个孩子。我应该担心它们吗?我正在使用Python 2.5。

我担心它们不会被垃圾收集并且应用程序最终会消耗所有内存。

6个回答

40

“担忧”是不必要的,但如果你的程序变得比预期更慢、消耗的内存超出预期或出现奇怪的暂停,原因确实可能在那些垃圾引用循环中——它们需要通过一种不同于“常规”(非循环)引用图的垃圾回收过程进行回收,并且如果你有大量对象被绑定在这样的循环中,那么该回收可能较慢(如果循环中的一个对象具有`__del__`特殊方法,则也会禁止循环垃圾回收)。

因此,引用循环不会影响程序的正确性,但可能会影响其性能和/或占用空间。

如果您想要删除不需要的引用循环,通常可以使用Python标准库中的weakref模块。

如果您想要更直接地控制(或执行调试,查看发生了什么),关于循环垃圾回收,请使用Python标准库中的gc模块。


关于 __del__ 的注释需要加上。如果你的对象析构函数有副作用,那么你可能需要更仔细地考虑循环引用(以及何时销毁)。 - speedplane

18

实验结果表明:你没问题:

import itertools

for i in itertools.count():
    a = {}
    b = {"a":a}
    a["b"] = b

它始终保持在使用3.6 MB的RAM。


你使用了哪种实现? - Display Name
@SargeBorsch CPython 2.x 版本。不过我猜想其他主要的实现方式也会表现出同样的行为。 - cobbal
这是短期对象生命周期,任何时候最多只使用2个。我们能在其他情况下期待相同的结果吗? - Jimmy T.
@JimmyT。简而言之:不,没有什么是保证的,你不能指望任何Python实现的特定垃圾回收策略。这可能取决于Python解释器的引用计数实现。因此,在使用CPython的任何情况下,并且没有闭包或其他引用抓住值的情况下,您可能可以依赖其行为,但在使用纯标记和扫描或停止和复制实现的情况下,您可能无法依赖它。简而言之,没有什么是保证的。这一切都取决于具体情况。许多不同的垃圾收集器都是有效的标准。 - cobbal

13

当没有外部引用时,Python会检测到循环引用并释放内存。


2
当然,假设没有__del__方法。通常不应该有,但你永远不知道。有一段时间,即使是collections.OrderedDict也有一个这样的方法,原因不明。 - Antimony

7

循环引用是一种常见的做法,因此我认为没有必要担心它们。许多树算法要求每个节点都有指向其子节点和父节点的链接。它们还需要实现类似于双向链表的东西。


1
谢谢 Colin。我不知道它们是“正常的事情”。对我来说,它们似乎非常特别。但现在我知道了。 :) - bodacydo
此外,它们显然对于图表是必需的。 - Antimony

3

在变量中的列表中引用方法似乎存在问题。以下是两个示例。第一个示例不调用__del__。使用weakref的第二个示例可以调用__del__,但在这种情况下问题是您无法弱引用方法:http://docs.python.org/2/library/weakref.html

import sys, weakref

class One():
    def __init__(self):
        self.counters = [ self.count ]
    def __del__(self):
        print("__del__ called")
    def count(self):
        print(sys.getrefcount(self))


sys.getrefcount(One)
one = One()
sys.getrefcount(One)
del one
sys.getrefcount(One)


class Two():
    def __init__(self):
        self.counters = [ weakref.ref(self.count) ]
    def __del__(self):
        print("__del__ called")
    def count(self):
        print(sys.getrefcount(self))


sys.getrefcount(Two)
two = Two()
sys.getrefcount(Two)
del two
sys.getrefcount(Two)

3
我认为你不需要担心。尝试下面的程序,你会发现它不会占用所有的内存:

我不认为你应该担心。尝试以下程序,你会发现它不会消耗所有的内存:

while True:
    a=range(100)
    b=range(100)
    a.append(b)
    b.append(a)
    a.append(a)
    b.append(b)

你是不是想说 a.extend(b) 而不是 append - richizy
4
@richizy,我真的是指“追加”(append),因为我希望在a和b内保存对a和b的引用,而不是它们的值。这样就会发生循环引用。 - douglaz

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