垃圾回收不删除WeakKeyDictionaries中的循环引用?
我遇到了一种情况,我想保持一个对象和另一个对象之间的关系,直到第一个对象被删除。我最开始想到的是用一个叫做WeakKeyDictionary的东西。
import weakref
import gc
class M:
pass
w = weakref.WeakKeyDictionary()
m = M()
w[m] = some_other_object
del m
gc.collect()
print w.keys()
在大多数情况下,这个方法都很好用。不过,如果some_other_object
是m
,或者它有指向m
的引用,那么M
这个实例就不会被垃圾回收了。(你可以用m
替换some_other_object
来看看这个例子)
当然,如果我把这个关系存储在对象本身上,当我删除这个对象时,它就会被垃圾回收:
import weakref
import gc
class M:
pass
m = M()
m.circular_reference = m
r = weakref.ref(m)
del m
gc.collect()
print r
我能不能用weakref模块来实现第二个例子的效果(也就是说,不去改变m
)呢?
换句话说,我能不能用weakref模块把一个对象映射到它自己(或者另一个有强引用的对象),并且只在有其他引用指向它的时候,才让这个对象保留在内存中?
2 个回答
处理这种循环引用的问题可能会让人感到头疼,但我还是会尽量回答你的问题。
在Python中,字典会存储它的键和对应值的引用。一个叫做WeakKeyDictionary的字典会对它的键存储弱引用,而对它的值存储强引用。所以在你的第一个例子中,如果你执行 w[m] = m
,那么字典 w
会对 m
作为键存储一个弱引用,同时对 m
作为值存储一个强引用。
Python的weakref模块还有一个叫做WeakValueDictionary的字典,它对键存储强引用,对值存储弱引用,但这并不能解决你的问题。
你真正需要的是一个字典,它对键和值都存储弱引用。下面的代码可以明确地让字典中的每个值都是一个弱引用:
import weakref
import gc
class M():
pass
class Other():
pass
m = M()
w = weakref.WeakKeyDictionary()
w[m] = weakref.ref(m)
print len(w.keys()) # prints 1
del m
print len(w.keys()) # prints 0
m = M()
o = Other()
o.strong_ref = m
w[m] = weakref.ref(o)
print len(w.keys()) # prints 1
del m
print len(w.keys()) # prints 1
del o
print len(w.keys()) # prints 0
你也可以通过子类化WeakKeyDictionary来自动让值变成弱引用。不过要注意,这并不像真正的WeakValueDictionary那样工作,因为它没有回调机制来通知字典某个值被删除了。
class MyWeakKeyValueDictionary(weakref.WeakKeyDictionary):
def __init__(self):
weakref.WeakKeyDictionary.__init__(self)
def __setitem__(self, key, value):
weakref.WeakKeyDictionary.__setitem__(self, key, weakref.ref(value))
w2 = MyWeakKeyValueDictionary()
m = M()
w2[m] = m
print len(w2.keys()) # prints 1
del m
print len(w2.keys()) # prints 0
m = M()
o = Other()
o.strong_ref = m
w2[m] = o
print len(w2.keys()) # prints 1
del m
print len(w2.keys()) # prints 1
del o
print len(w2.keys()) # prints 0
不过,最后我担心处理这种循环引用可能会带来更多问题,得不偿失。
在这些例子中,其实并没有真正的循环引用。你的循环是这样的:
WeakKeyDict -> 值 -> 键 -> ...
所以这个字典(dict)保持了值的存在,而这个值又保持了键的存在。通过一种不涉及强引用的方式,键告诉字典要保持值的存在。因为这里没有真正的引用循环,所以垃圾回收器(GC)不会发现任何问题。(不过,你的第二个例子确实包含了循环引用,这就是它表现得像第一个例子的原因)
我猜解决你问题的唯一方法是确保字典中的值永远不会对任何键有强引用(直接或间接)。这和srgerg提到的类似,但你真正想要的是对键的引用是弱引用,而不是对所有值的弱引用。例如:
import weakref
import gc
class M:
pass
w = weakref.WeakKeyDictionary()
key = M()
val = M()
val.keyRef = weakref.ref(val)
w[key] = val
print len(w) ## prints 1
del key
gc.collect()
print len(w) ## prints 0
这样,值总是有强引用,但你在小心地控制对键的引用,以决定哪些内容会从字典中移除。根据你程序的复杂性,这可能需要花费不少时间来实现,因为你需要手动追踪所有对键的引用。
不过,如果你能告诉我们更多关于具体问题的信息,也许我们可以提出一个更简单的解决方案。