循环引用 - 中断单个引用

Circular reference - break on single reference

TL;DR 有没有办法创建一个弱引用,它会在剩下 1 个强引用而不是 0 个强引用时调用回调?


对于那些认为这是 X Y 问题的人,这里有详细的解释:

我有一个相当具有挑战性的问题,我正试图用我的代码解决。

假设我们有一个 class Foo 的实例,以及一个不同的 class Bar,它在使用实例时引用了它:

class Foo:  # Can be anything
    pass

class Bar:
    """I must hold the instance in order to do stuff"""
    def __init__(self, inst):
        self.inst = inst

foo_to_bar = {}
def get_bar(foo):
    """Creates Bar if one doesn't exist"""
    return foo_to_bar.setdefault(foo, Bar(foo))

# We can either have
bar = get_foobar(Foo())
# Bar must hold a strong reference to foo

# Or
foo = Foo()
bar = get_foobar(foo)
bar2 = get_foobar(foo)  # Same Bar
del bar
del bar2
bar3 = get_foobar(foo)  # Same Bar
# In this case, as long as foo exists, we want the same bar to show up,
# therefore, foo must in some way hold a strong reference back to bar

现在是棘手的部分:您可以使用循环引用解决此问题,其中 foo 引用 barbar 引用 foo,但是,嘿,这是什么有趣的部分是什么?清理需要更长的时间,如果 Foo 定义了 __slots__ 并且通常是一个糟糕的解决方案,则无法正常工作。

有什么方法可以创建一个 foo_to_bar 映射来清除对 foobar 单个 引用?本质上:

import weakref
foo_to_bar = weakref.WeakKeyDictionary()
# If bar is referenced only once (as the dict value) and foo is
# referenced only once (from bar.inst) their mapping will be cleared out

这样它可以完美地工作,因为在函数之外有 foo 确保 bar 仍然存在(我可能需要 Foo 上的 __slots__ 来支持 __weakref__) 并且 bar 在函数外部导致 foo 仍然存在(因为 Bar 中的强引用)。

WeakKeyDictionary 不起作用,因为 {weakref.ref(inst): bar.inst} 会导致循环引用。

或者,有没有什么方法可以连接到引用计数机制(以便在两个对象都达到 1 个引用时进行清理)而不会产生重大开销?

你想多了。如果只剩下一个参考,则无需跟踪。你的错误是首先创建了一个循环引用。

在您的缓存中存储 _BarInner 个对象,这些对象没有引用 Foo 个实例。访问映射后,return 一个包含 _BarInnerFoo 引用的轻量级 Bar 实例:

from weakref import WeakKeyDictionary
from collections.abc import Mapping


class Foo:
    pass


class Bar:
    """I must hold the instance in order to do stuff"""
    def __init__(self, inst, inner):
        self._inst = inst
        self._inner = inner

    # Access to interesting stuff is proxied on to the inner object,
    # with the instance information included *as needed*.
    @property
    def spam(self):
        self.inner.spam(self.inst)


class _BarInner:
    """The actual data you want to cache"""
    def spam(self, instance):
        # do something with instance, but *do not store any references to that
        # object on self*.


class BarMapping(Mapping):
    def __init__(self):
        self._mapping = WeakKeyDictionary()

    def __getitem__(self, inst):
        inner = self._mapping.get(inst)
        if inner is None:
            inner = self._mapping[inst] = _BarInner()
        return Bar(inst, inner)

将此翻译成评论中链接的 bdict project,您可以大大简化事情:

  • 不用担心项目中缺乏对弱引用的支持。记录您的项目将仅支持具有 __weakref__ 属性的类型的实例数据。够了。
  • 不区分槽型和非槽型。始终将每个实例的数据存储在远离实例的地方。这可以让您简化代码。
  • 'strong' 和 'autocache' 标志也是如此。享元应该始终保持强引用。应始终存储每个实例的数据。
  • 对描述符 return 值使用单个 class。 ClassBoundDict 类型就是您所需要的。将传递给 __get__instanceowner 数据存储在该对象中,并相应地改变 __setitem__ 中的行为。
  • 查看 collections.ChainMap() 以封装对 class 的访问和读取访问的实例映射。