Python中的循环引用

4
我不确定 Python 如何处理循环引用(引用循环)。我查看了一些答案,发现以下内容:这里
Python 的标准引用计数机制无法释放环,因此您示例中的结构将会泄漏。 然而,补充垃圾收集设施已启用,默认情况下应该能够释放该结构,前提是它的所有组件都不再由外部可达并且它们没有 __del__() 方法。
我猜这意味着如果引用循环中的任何实例在外部都不可达,则两者都将被清理。这是真的吗?
另一方面,有一个名为 weakref 的包经常用于处理映射字典。我认为它存在的目的是避免引用循环。
总之,Python 能否自动处理引用循环?如果可以,为什么我们还要使用 weakref?
3个回答

6
如果循环引用中的对象没有自定义__del__方法,那么您不必担心引用循环问题,因为Python可以(并且会)以任何顺序销毁这些对象。如果您的自定义方法确实有__del__方法,则Python不知道一个对象的删除是否会影响其他对象的删除。例如,当删除一个对象时,它设置了一些全局变量,因此对象仍然存在。您可以创建一个打印输出内容的__del__方法进行快速测试:
class Deletor(str):
    def __del__(self):
        print(self, 'has been deleted')

a = Deletor('a')  # refcount: 1
del a             # refcount: 0

输出:

a has been deleted

但是如果你有这样的代码:

a = Deletor('a')  # refcount: 1
a.circular = a    # refcount: 2
del a             # refcount: 1

由于 Python 无法安全删除 a,因此它不会输出任何内容。它变成了"不可回收对象",可以在gc.garbage中找到。

这里有两个解决方案,其中一个是使用 weakref(它不会增加引用计数):

# refcount:             a  b
a = Deletor('a')      # 1  0
b = Deletor('b')      # 1  1
b.a = a               # 2  1
a.b = weakref.ref(b)  # 2  1
del a                 # 1  1
del b                 # 1  0
# del b kills b.a     # 0  0

输出:

b has been deleted
a has been deleted

(注意在删除a之前先删除b)
您也可以手动删除循环(如果您能跟踪它们):
# refcount          a  b
a = Deletor('a')  # 1  0
b = Deletor('b')  # 1  1
b.a = a           # 2  1
a.b = b           # 2  2
del b             # 2  1
print('del b')
del a.b           # 2  0
# b is deleted, now dead
# b.a now dead    # 1  0
print('del a.b')
del a             # 0  0
print('del a')

输出:

del b
b has been deleted
del a.b
a has been deleted
del a

注意在删除 a.b 之后,b 才会被删除。
† 从 Python 3.4 开始,由于 PEP 442 的原因,一些事情发生了变化。即使是处于引用循环中的对象,也可能调用 __del__,并且语义略有不同,因此变成无法收集的对象稍微困难一些。 weakref 仍然有用,因为它对垃圾回收器的影响较小,并且可以更早地回收内存。

1
它不会输出任何内容,因为Python不能安全地删除a。这并不是真的(或者至少令人困惑)。运行“import gc; gc.collect()”,它将被安全地删除。 - AXO
1
@AXO 我似乎是用 Python 2 或 Python 3.4 之前的版本编写了这个代码(不得不承认,Python 3.4 是在2014年发布的)。我已经更新了答案以反映这一点。 - Artyer

2

(试图回答为什么我们需要弱引用的子问题。)

弱引用不仅可以打破循环引用,还可以防止不必要的非循环引用。

我最喜欢的例子是使用 WeakSet 计算同时网络连接数(一种负载测量)。在这个例子中,每个新连接都必须添加到 WeakSet 中,但这是网络代码需要完成的唯一任务。连接可以由服务器、客户端或错误处理程序关闭,但是这些例程都没有责任从集合中删除连接,这是因为附加的引用是弱引用。


2
变量是内存引用。
 my_var=10 

这个值被存储在内存槽中。 my_var 实际上引用了存储数字10的内存槽地址。如果您键入:

id(my_var) 

您将获得基于十进制的插槽地址。 hex(id(my_var)) 将给出地址的十六进制表示。

每当我们使用 my_var 时,Python 内存管理器会访问内存并检索值为 10 的变量。Python 内存管理器还会跟踪此内存插槽的引用数量。如果没有对该内存地址的引用,则 Python 内存管理器会销毁该对象,并将此内存插槽用于新对象。

假设我们有两个类:

class A:
    def __init__(self):
        self.b = B(self)
        print('A: self: {0}, b:{1}'.format(hex(id(self)), hex(id(self.b))))

class B:
    def __init__(self, a):
        self.a = a
        print('B: self: {0}, a: {1}'.format(hex(id(self)), hex(id(self.a))))

当你定义类A的一个实例时:

   my_var = A()

你将会得到以下打印结果:(在你的系统中,地址可能不同)
B: self: 0x1fc1eae44e0, a: 0x1fc1eae4908
A: self: 0x1fc1eae4908, b:0x1fc1eae44e0

注意参考文献。它们存在循环引用。

注意:为了查看这些引用,您需要禁用垃圾收集器,否则它会自动删除它们。

  gc.disable()

目前,指针(0x1fc1eae4908)的引用计数为 2。my_var 和 classB 都在引用此地址。如果我们更改 my_var,则...

   my_var= None

现在my_var指向的内存地址不同了。现在(0x1fc1eae4908)的引用计数为1,因此这个内存槽没有被清除。

现在我们将会有内存泄漏,即当不再需要内存时,它没有被清理。

垃圾回收器会自动识别循环引用中的内存泄漏并清理它们。但是如果循环引用中的任何一个对象具有析构函数 (del()),垃圾回收器就无法知道对象的销毁顺序。因此,该对象被标记为不可回收,循环引用中的对象将不会被清理,从而导致内存泄漏

weakref用于缓存目的。我认为Python有非常好的文档。

这里是weakref的参考资料:

weakref文档


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