如何以及何时在Python中正确使用weakref

60 投票
3 回答
16975 浏览
提问于 2025-04-15 14:44

我有一些代码,其中类的实例之间有父子关系,比如:

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()完成后,rootc1c2就不会被释放,对吧?那么,我该怎么让它们被释放呢?我想我可以做一些像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

我想澄清一下哪些引用可以是弱引用。下面的方法是通用的,但我在所有示例中都使用双向链表树。

逻辑步骤 1.

你需要确保有强引用来保持所有对象的存活,直到你需要它们为止。这可以通过多种方式实现,比如:

  • [直接名称]:对树中每个节点的命名引用
  • [容器]:对存储所有节点的容器的引用
  • [根节点 + 子节点]:对根节点的引用,以及每个节点到其子节点的引用
  • [叶子节点 + 父节点]:对所有叶子节点的引用,以及每个节点到其父节点的引用

逻辑步骤 2.

现在你可以添加引用来表示信息,如果需要的话。

例如,如果你在步骤 1 中使用了 [容器] 方法,你仍然需要表示节点之间的连接。节点 A 和 B 之间的连接可以用一个引用来表示,连接的方向可以是任意的。同样,有很多选择,比如:

  • [子节点]:每个节点到其子节点的引用
  • [父节点]:每个节点到其父节点的引用
  • [集合的集合]:一个包含 2 个元素集合的集合;每个 2 元素集合包含一个边的节点引用

当然,如果你在步骤 1 中使用了 [根节点 + 子节点] 方法,你的信息已经完全表示出来了,所以可以跳过这一步。

逻辑步骤 3.

现在你可以添加引用来提高性能,如果需要的话。

例如,如果你在步骤 1 中使用了 [容器] 方法,在步骤 2 中使用了 [子节点] 方法,你可能希望提高某些算法的速度,并在每个节点和其父节点之间添加引用。这种信息在逻辑上是多余的,因为你可以(虽然会影响性能)从现有数据中推导出来。


步骤 1 中的所有引用必须是强引用

步骤 2 和 3 中的所有引用可以是弱引用或强引用。使用强引用没有好处。使用弱引用有优势,直到你确定循环不再可能。严格来说,一旦你知道循环不可能,就无论使用弱引用还是强引用都没有区别。但为了避免思考这个问题,你可以在步骤 2 和 3 中只使用弱引用。

21

我建议使用 child.parent = weakref.proxy(self)。这个方法可以很好地避免循环引用的问题,特别是当 parent 的生命周期比 child 长的时候。还有,当 child 的生命周期比 parent 长时,可以使用 self.children = weakref.WeakValueDictionary()(正如 Alex Martelli 提到的)。但是,如果 parentchild 都可以独立存在,就不要使用弱引用。接下来会用例子来说明这些规则。

如果你把根节点绑定到一个名字上并传递它,同时从中访问子节点,就要使用弱引用的父节点:

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
33

没错,使用弱引用在这里非常好。具体来说,不要这样写:

self.children = {}

而是用:

self.children = weakref.WeakValueDictionary()

你的代码其他部分不需要改动。这样,当一个子对象没有其他引用时,它就会消失——而且在父对象的children映射中,那个子对象的条目也会被删除。

避免引用循环是使用weakref模块的重要原因之一,和实现缓存一样重要。引用循环虽然不会直接导致程序崩溃,但可能会占用你的内存,尤其是当参与循环的某些类定义了__del__时,这会干扰垃圾回收(gc)模块处理这些循环的能力。

撰写回答