在Python中什么时候使用弱引用?

42 投票
3 回答
8694 浏览
提问于 2025-04-15 20:22

有人能解释一下弱引用的用法吗?

文档里没有详细说明,只是说垃圾回收(GC)可以随时销毁通过弱引用链接的对象。那么,为什么还要有一个随时可能消失的对象呢?如果我在它消失后想用它怎么办?

能不能给我一些好的例子来解释一下?

谢谢

3 个回答

1
  • 弱引用是Python中的一个重要概念,而在像Java(Java 1.5)这样的语言中是缺失的。
  • 在观察者设计模式中,通常可观察对象(Observable Object)必须对观察者对象(Observer)保持弱引用。

    举个例子,假设A发出一个事件done(),而B注册到A上,表示它想要监听这个done()事件。这样,每当done()被触发时,B就会收到通知。但是,如果B在应用中并不是必须的,那么A就不应该阻碍垃圾回收(因为A持有对B的引用)。因此,如果A对B保持的是弱引用,当所有指向A的引用都消失时,B就会被垃圾回收。

  • 弱引用在实现缓存时也非常有用。
48

事件是使用弱引用的一个常见场景。


问题

想象一下有两个对象:发射器(Emitter)和接收器(Receiver)。接收器的生命周期比发射器短。

你可以尝试这样实现:

class Emitter(object):

    def __init__(self):
        self.listeners = set()

    def emit(self):
        for listener in self.listeners:
            # Notify
            listener('hello')


class Receiver(object):

    def __init__(self, emitter):

        emitter.listeners.add(self.callback)

    def callback(self, msg):
        print 'Message received:', msg


e = Emitter()
l = Receiver(e)
e.emit() # Message received: hello

但是在这种情况下,发射器保持着一个绑定方法 callback 的引用,而这个方法又保持着接收器的引用。所以发射器让接收器一直活着:

# ...continued...

del l
e.emit() # Message received: hello

这有时候会带来麻烦。想象一下,Emitter 是某个数据模型的一部分,用来指示数据何时变化,而 Receiver 是通过一个对话框创建的,这个对话框监听这些变化以更新一些用户界面控件。

在应用程序的运行过程中,可能会打开多个对话框,我们不希望它们的接收器在窗口关闭后仍然被发射器注册着。那样会造成内存泄漏。

手动移除回调是一个选项(这也很麻烦),使用弱引用是另一个选择。


解决方案

有一个很不错的类 WeakSet,看起来像普通的集合,但它使用弱引用来存储成员,当成员被释放时,它就不再存储它们了。

太好了!我们来用它:

def __init__(self):
    self.listeners = weakref.WeakSet()

然后再运行一次:

e = Emitter()
l = Receiver(e)
e.emit()
del l
e.emit()

哦,什么都没发生!这是因为绑定的方法(特定接收器的 callback)现在变成孤儿了——发射器和接收器都没有强引用指向它。因此它会立即被垃圾回收。

现在我们让接收器(这次不是发射器)保持对这个回调的强引用:

class Receiver(object):

    def __init__(self, emitter):

        # Create the bound method object
        cb = self.callback

        # Register it
        emitter.listeners.add(cb)
        # But also create an own strong reference to keep it alive
        self._callbacks = set([cb])

现在我们可以观察到预期的行为:发射器只在接收器存在时才保持对回调的引用。

e = Emitter()
l = Receiver(e)
assert len(e.listeners) == 1

del l
import gc; gc.collect()
assert len(e.listeners) == 0

底层原理

注意,我在这里加了一个 gc.collect(),以确保接收器真的能立即被清理。这是因为现在有一个强引用的循环:绑定的方法引用了接收器,而接收器又引用了这个方法。

这并不是很糟糕;这只是意味着接收器的清理会推迟到下一个垃圾回收的运行。循环引用不能通过简单的引用计数机制来清理。

如果你真的想,你可以通过用一个自定义函数对象替换绑定的方法,来打破这个强引用的循环,这个自定义函数对象也会将它的 self 作为弱引用。

def __init__(self, emitter):

    # Create the bound method object
    weakself = weakref.ref(self)
    def cb(msg):
        self = weakself()
        self.callback(msg)

    # Register it
    emitter.listeners.add(cb)
    # But also create an own strong reference to keep it alive
    self._callbacks = set([cb])

让我们把这个逻辑放到一个辅助函数中:

def weak_bind(instancemethod):

    weakref_self = weakref.ref(instancemethod.im_self)
    func = instancemethod.im_func

    def callback(*args, **kwargs):
        self = weakref_self()
        bound = func.__get__(self)
        return bound(*args, **kwargs)

    return callback

class Receiver(object):

    def __init__(self, emitter):

        cb = weak_bind(self.callback)

        # Register it
        emitter.listeners.add(cb)
        # But also create an own strong reference to keep it alive
        self._callbacks = set([cb])

现在没有强引用的循环了,所以当 Receiver 被释放时,回调函数也会立即被释放(并从发射器的 WeakSet 中移除),不需要完整的垃圾回收周期。

28

弱引用的常见用途是当对象A引用对象B,而对象B又引用对象A时。如果没有一个能检测循环引用的垃圾回收器,这两个对象就算外面没有其他地方引用它们,也永远不会被清理掉。但是如果其中一个引用是“弱引用”,那么这些对象就能被正常清理了。

不过,Python其实是有循环检测的垃圾回收器的(从2.0版本开始就有了!),所以这个情况在Python里不算问题哦 :)

弱引用的另一个用途是用来做缓存。在weakref的文档中提到:

弱引用的一个主要用途是实现缓存或映射,存放一些大对象,这样就可以确保大对象不会因为在缓存或映射中而被一直保留。

如果垃圾回收器决定要销毁其中一个对象,而你又需要它,你可以重新计算或重新获取这些数据。

撰写回答