即使没有非弱引用指向一个对象,弱引用是否可以不等于None?

3
据我所知,Python 3没有对对象的内存何时释放做出任何保证,除非有至少一个非弱引用指向该对象,否则不会释放。
那么语言是否对弱引用做出其他保证呢?特别是,假设我创建了一个弱引用“wr”指向某个对象。后来所有非弱引用都已被销毁,那么在那时,“wr”将会被保证评估为None,还是可能一段时间内仍然评估为原始对象?
除了语言保证外,是否存在与weakref相关的有趣实现行为?

1
我希望你不打算编写依赖于这种行为的精细代码。就我个人而言,我从未需要使用weakref - Karl Knechtel
我经常在 PyQt 中使用 WeakKeyDictionary 和 WeakValueDictionary 来引用 Widgets,以避免它们被垃圾回收机制销毁。但是...我从不依赖于它们的有效性。您不应该期望弱引用在所有非弱引用数归零后仍然保持有效。 - jdi
@KarlKnechtel:我想避免依赖任何不确定的事情。如果它们不是特定于实现的,而是由语言保证的话,我不介意依赖于细微差别。 - max
4个回答

1
一个弱引用对象只有在GC启动后才会被销毁。由于这是非确定性的,不能保证它会在所有强引用都被删除后立即被销毁。
docs中可以看到:

...当对某个引用的最后一个引用是弱引用时,垃圾回收器可以自由地销毁该引用并将其内存重用于其他用途。

关键词是“可以自由地销毁该引用”,但不一定会立即发生。

我曾认为对象的销毁是首先发生的,并且是确定性的;但是内存稍后由GC释放,这是不确定性的。但现在我想想,如果它们不同时完成,那就很奇怪了 - 如果解释器已经知道对象被销毁,为什么不释放内存呢? - max

1
不,Python 不保证对象何时被收集,弱引用将返回 None。它可能会立即发生(在使用引用计数加循环引用垃圾回收器的 CPython 中经常发生,但在其他不使用引用计数的 Python 实现中发生的频率要少得多)。也可能因为多种原因而延迟 - 当不使用 CPython 时(或者使用不使用引用计数的 CPython 版本),或者当使用引用计数但您的对象参与引用循环时。

这是从您在https://dev59.com/x2HVa4cB1Zd3GeqPqMUO#9796689的回答中自动得出的结论,对吗?您在那里解释说`__del__`可能在最后一个强引用被删除后任意长时间内不会被调用。而且,直到`__del__`完成(即使只是因为`__del__`仍然可以恢复对象),weakref不能返回`None`。 - max

0
weakref 可以返回一个有效的对象(从 Python 的角度看),或者返回 None。实际上,何时注销对象(导致弱引用变为 None)是未定义的,除非没有任何其他活动对象对该对象进行非弱引用。您在 CPython 中观察到的引用计数行为并不保证。

1
谢谢!你的评论是否也意味着 weakref 直到对象实际被释放才会评估为 None?如果存在这样的保证,那么它也意味着如果一个 weakref 评估为 None,则所有 weakref 都评估为 None。 - max
翻转弱引用的代码作为解除分配的一部分运行。弱引用彼此链接并链接到对象。但这可能是一个实现细节 - 我不想保证在对象解除分配之前弱引用永远不会变成None - 特别是因为解除分配本身不是语言标准中的概念 - 重要的是对象是否不可达。最灰色的地带可能是是否有时可以通过跟随弱引用来恢复否则无法访问的对象 - 我不想说那不可能发生。 - Guido van Rossum
为什么您认为这是一个灰色地带?如果允许弱引用在最后一个强引用消失后保持有效,似乎不仅可能发生,而且肯定会发生。此时,如果我创建一个强引用,我刚刚恢复了对象,对吗(即Python现在可能不会销毁它)?我有什么遗漏的吗?我想Python可以捕获将弱引用分配给强引用的尝试,并在分配之前将弱引用更新为None。但这种行为似乎违背了检查强引用存在从未紧急的想法。 - max
我想说的是,并没有规定Python需要急于销毁没有强引用的对象。CPython实际上会急于销毁,除非该对象是循环的一部分。但是其他Python实现可能使用不立即检测到无活动引用的GC,然后可以通过弱引用来恢复对象。但是这种“复活”并没有做任何事情,因为尚未检测到缺少强引用,因此对象尚未被销毁。因此,具有0个强引用的对象仍然可能处于活动状态。就是这样。 - Guido van Rossum
谢谢,这让所有语言保证都变得非常清晰。 - max
哦,恰恰相反。这个答案没有区分存活性和可达性,因此表面上看,它暗示了在Python中像在JavaScript中一样,垃圾回收的资格同样是一个未明确规定的混乱;比较https://github.com/tc39/ecma262/issues/2650。虽然另一方面,实际上主要的实现似乎最终会以可达性为基础来定义GC的资格。但这并没有任何保证。 - user3840170

0

由于根据Guido的帖子,似乎不能保证所有弱引用都会被一致地释放。你可以做的一件事是在每个生成新弱引用的位置使用单个弱引用(因为对于所有使用它的人来说,单个引用必须看起来相同)。

这样,您将失去所有不同引用之间的漂亮回调机制,但是您可以通过子类化weakref.ref并添加某种注册方案来恢复它们。您还需要一些方法来定位weakref的单个实例。要么将其与要引用的实例打包并创建一个帮助程序方法来获取它,要么创建一个单例来查找它们(使用第一种方法...)。请注意线程安全问题,如果不想使用锁,请使用带有默认值的getattr。

为了实现合理的记忆方案,弱引用中的一致性消失似乎应该得到保证,但是使用单个弱引用应该可以工作,尽管需要重新实现解释器的weakref方案以获得所需的保证。

实际上,您可以使用.__weakref__属性,在解释器中的实例上显示弱引用,并使用该帮助函数方法。


为什么“nullifying”弱引用的时间是个问题?只要我们知道它不保证在同一时间发生,所以我们不依赖它,似乎没问题。你认为会导致备忘录(memoization)出现问题的情况是什么? - max
假设两个不同的地方共享一个记忆化缓存,并且每个地方都携带一个弱引用,每个地方都知道在弱引用翻转时重建/启动新缓存。如果不能保证它们同时返回None,它们可能会开始使用不同的缓存。我承认这有点牵强,实际上在cPython中可能永远不会出现,但我想这是一种强制执行期望的方式,即如果一个弱引用返回None,则所有弱引用也都是如此。 - Lee McCuller
难道(唯一的)缓存管理器对象不应该与强引用链接,只有从该管理器才会有一个(单一的)弱引用指向实际数据位置吗?也许我错过了多个弱引用设计的好理由,抱歉 - 我正在学习这些东西。 - max

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接