如何以及何时在Python中正确使用weakref
我有一些代码,其中类的实例之间有父子关系,比如:
class Node:
def __init__(self):
self.parent = None
self.children = {}
def AddChild(self, name, child):
child.parent = self
self.children[name] = child
def Run():
root, c1, c2 = Node(), Node(), Node()
root.AddChild('first', c1)
root.AddChild('second', c2)
Run()
我觉得这会造成循环引用,这样在Run()完成后,root
、c1
和c2
就不会被释放,对吧?那么,我该怎么让它们被释放呢?我想我可以做一些像root.children.clear()
或者self.parent = None
的操作,但如果我不知道什么时候该这么做呢?
这时候适合使用weakref模块吗?我到底应该对哪个部分使用weakref?是parent
属性?还是children
属性?还是整个对象?还是以上所有的?我看到有人提到WeakKeyDictionary和weakref.proxy,但我不太明白在这种情况下它们应该怎么用,如果有的话。
这段代码是在Python 2.4上(不能升级)。
更新:示例和总结
要使用weakref的对象取决于哪个对象可以独立于另一个对象存在,以及哪些对象之间是相互依赖的。存活时间最长的对象应该包含对存活时间较短对象的weakref。同样,依赖关系不应该使用weakref——如果使用了,依赖对象可能会在仍然需要的情况下悄悄消失。
例如,如果你有一个树结构,root
有孩子kids
,但可以没有孩子存在,那么root
对象应该对它的kids
使用weakref。如果子对象依赖于父对象的存在,那么也应该使用weakref。下面,子对象需要一个父对象来计算它的深度,因此对parent
使用强引用。不过,kids
属性中的成员是可选的,所以使用weakref来防止循环引用。
class Node:
def __init__(self):
self.parent = None
self.kids = weakref.WeakValueDictionary()
def GetDepth(self):
root, depth = self, 0
while root:
depth += 1
root = root.parent
return depth
root = Node()
root.kids['one'] = Node()
root.kids['two'] = Node()
如果要反转这种关系,我们可以这样做。在这里,Facade
类需要一个Subsystem
实例才能工作,所以它们对所需的子系统使用强引用。然而,Subsystem
并不需要Facade
才能工作。Subsystem
只是提供了一种通知Facade
彼此动作的方式。
class Facade:
def __init__(self, subsystem):
self.subsystem = subsystem
subsystem.Register(self)
class Subsystem:
def __init__(self):
self.notify = []
def Register(self, who):
self.notify.append(weakref.proxy(who))
sub = Subsystem()
cli = Facade(sub)
3 个回答
我想澄清一下哪些引用可以是弱引用。下面的方法是通用的,但我在所有示例中都使用双向链表树。
逻辑步骤 1.
你需要确保有强引用来保持所有对象的存活,直到你需要它们为止。这可以通过多种方式实现,比如:
- [直接名称]:对树中每个节点的命名引用
- [容器]:对存储所有节点的容器的引用
- [根节点 + 子节点]:对根节点的引用,以及每个节点到其子节点的引用
- [叶子节点 + 父节点]:对所有叶子节点的引用,以及每个节点到其父节点的引用
逻辑步骤 2.
现在你可以添加引用来表示信息,如果需要的话。
例如,如果你在步骤 1 中使用了 [容器] 方法,你仍然需要表示节点之间的连接。节点 A 和 B 之间的连接可以用一个引用来表示,连接的方向可以是任意的。同样,有很多选择,比如:
- [子节点]:每个节点到其子节点的引用
- [父节点]:每个节点到其父节点的引用
- [集合的集合]:一个包含 2 个元素集合的集合;每个 2 元素集合包含一个边的节点引用
当然,如果你在步骤 1 中使用了 [根节点 + 子节点] 方法,你的信息已经完全表示出来了,所以可以跳过这一步。
逻辑步骤 3.
现在你可以添加引用来提高性能,如果需要的话。
例如,如果你在步骤 1 中使用了 [容器] 方法,在步骤 2 中使用了 [子节点] 方法,你可能希望提高某些算法的速度,并在每个节点和其父节点之间添加引用。这种信息在逻辑上是多余的,因为你可以(虽然会影响性能)从现有数据中推导出来。
步骤 1 中的所有引用必须是强引用。
步骤 2 和 3 中的所有引用可以是弱引用或强引用。使用强引用没有好处。使用弱引用有优势,直到你确定循环不再可能。严格来说,一旦你知道循环不可能,就无论使用弱引用还是强引用都没有区别。但为了避免思考这个问题,你可以在步骤 2 和 3 中只使用弱引用。
我建议使用 child.parent = weakref.proxy(self)
。这个方法可以很好地避免循环引用的问题,特别是当 parent
的生命周期比 child
长的时候。还有,当 child
的生命周期比 parent
长时,可以使用 self.children = weakref.WeakValueDictionary()
(正如 Alex Martelli 提到的)。但是,如果 parent
和 child
都可以独立存在,就不要使用弱引用。接下来会用例子来说明这些规则。
如果你把根节点绑定到一个名字上并传递它,同时从中访问子节点,就要使用弱引用的父节点:
def Run():
root, c1, c2 = Node(), Node(), Node()
root.AddChild('first', c1)
root.AddChild('second', c2)
return root # only root refers to c1 and c2 after return,
# so this references should be strong
如果你把每个子节点绑定到一个名字上并传递它们,同时从中访问根节点,就要使用弱引用的子节点:
def Run():
root, c1, c2 = Node(), Node(), Node()
root.AddChild('first', c1)
root.AddChild('second', c2)
return c1, c2
在这种情况下,不要使用弱引用:
def Run():
root, c1, c2 = Node(), Node(), Node()
root.AddChild('first', c1)
root.AddChild('second', c2)
return c1
没错,使用弱引用在这里非常好。具体来说,不要这样写:
self.children = {}
而是用:
self.children = weakref.WeakValueDictionary()
你的代码其他部分不需要改动。这样,当一个子对象没有其他引用时,它就会消失——而且在父对象的children
映射中,那个子对象的条目也会被删除。
避免引用循环是使用weakref
模块的重要原因之一,和实现缓存一样重要。引用循环虽然不会直接导致程序崩溃,但可能会占用你的内存,尤其是当参与循环的某些类定义了__del__
时,这会干扰垃圾回收(gc
)模块处理这些循环的能力。