理解“底层的C/C++对象已被删除”错误

23

这不是我第一次遇到 RuntimeError: underlying C/C++ object has been deleted 错误。我以前曾多次通过随意但直观地修改代码来解决它,但现在我又面临着这个问题,只是不明白为什么会发生这种情况......

我不会在这里发布代码示例,因为我的项目太复杂了,我只是无法找出错误所在。而且我要求通用的解决方案,而不仅仅是针对这种情况。

为什么“底层的C/C++”对象会被删除?
如何避免这种情况发生?
如何测试底层对象是否存在?


这之前有任何错误输出吗? - Bart
@Bart:不,一切都清楚,这是第一个错误。 - Rizo
5个回答

15
您无法测试底层对象是否存在(例如它是否已被“删除”)。虽然有一些“技巧”可以用来“部分”解决这个问题,但我不建议这样做。相反,在我看来,我猜测您的设计缺乏强大的所有权语义。(根据问题的复杂性和您的领域,这可能是必要的,但如果可避免,我建议不要这样做。)
您的问题在于所有权语义。特别是在C++中,与使用内存“垃圾回收”的其他语言相比,您必须拥有一个清楚的“谁拥有什么”的强大理解。您的设计应该使人们很容易地知道哪些对象存在以及谁“拥有”它们。
通常,这意味着将对象的“创建”和“删除”集中到某种“父”中,这样就永远不会有任何问题,“这是谁的对象?”和“你管理的对象是什么?”。
例如,“父”类将分配其所需的成员对象,只有父级才能删除这些对象。父类的析构函数确保这些成员被删除,因此这些对象不会意外地“残留”。因此,父类“知道”它的成员对象,当父类消失时,您也知道成员对象也消失了。如果您的设计紧密耦合,则成员对象可能会引用父对象,但这是所有权语义的“深度”,通常不建议这样做。
有些设计可以合法地非常复杂。然而,总的来说,所有权语义永远不应该是复杂的:您应该始终知道“谁拥有什么”,因此,您应该始终知道“什么对象存在”。
在具有垃圾回收语言的情况下,还可能出现其他设计,其中对象可能是“自由代理”,您信任垃圾收集器处理已经迷失跟踪的对象的“繁重工作”。 (我不偏爱这些设计,但根据独特的问题领域,承认它们有时在那些其他语言中是可以接受的。)如果你的C++设计依赖于这样的“自由代理”对象,你可以编写自己的垃圾收集器或“实用所有者”来清理它们。例如,MyFreeAgent构造函数可以在MyGarbageCollector中注册自己,MyFreeAgent析构函数会从MyGarbageCollector中注销自己。因此,MyFreeAgent实例可以由MyGarbageCollector进行“清理”,或者MyFreeAgent甚至可以使用"delete this;"自己清除,并且你会知道它已经被清除了(因为其析构函数会从MyGarbageCollector中注销自己)。在这种设计中,单个MyGarbagageCollector将始终“知道”存在哪些MyFreeAgent实例。
这样的设计处于“深水区”,应该极度谨慎地使用,即使它们能够工作,因为它们有很大的潜力来彻底重构您的系统。(我已经使用过它们,但是你必须确定问题确实需要这样的设计。)

2
有人能把这个翻译成Python吗? - eric
请参见下面由ekhumoro提供的答案,其中提供了简明扼要的解释和非常有用的示例。 - djvg

10

当前接受的答案声称:

您无法测试底层对象是否存在(例如,它是否已被“删除”)。

这是错误的。一定有一种方法可以做到这一点,否则 PyQt 就无法引发异常。以下是一些示例输出,演示了如何明确地测试删除:

>>> import sip
>>> from PyQt4 import QtCore, QtGui
>>> app = QtGui.QApplication([''])
>>> w = QtGui.QWidget()
>>> w.setAttribute(QtCore.Qt.WA_DeleteOnClose)
>>> sip.isdeleted(w)
False
>>> w.close()
True
>>> sip.isdeleted(w)
True
>>> w.objectName()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: wrapped C/C++ object of type QWidget has been deleted

PyQt窗口小部件由Python部分和C++部分组成。如果C++部分被Qt删除,那么将会留下一个空的Python封装对象。如果你试图通过处于这种状态的对象调用任何Qt方法,就会引发一个RuntimeError(当然,这比可能的段错误更可取)。

一般来说,Qt不倾向于隐式删除对象,这就是为什么我在上面的示例中必须显式标记该小部件进行删除的原因。这个一般规则的例外情况总是清楚地记录在文档中 - 例如指定是Qt还是调用者拥有对象的所有权。 (这就是为什么即使你没有C ++知识,熟悉Qt文档也可能真正有所回报的原因之一)。

在大多数PyQt代码中,避免问题的最简单方法是确保对象具有父级或显式保留对对象的引用(通常作为主窗口的实例属性)。


似乎保存变量的引用并不能防止对象被删除。例如,我有一个主窗口和对话框。在主窗口中,我创建了一些小部件并将其保存在窗口属性中。在对话框构造函数中,我从主窗口获取小部件并将其放入对话框布局中。如果我关闭对话框并再次打开它,我会看到对象已删除错误。为什么对话框析构函数会删除它的小部件,尽管它在另一个变量中有引用? - Artem
另外想要添加的是,我只在Windows上遇到了这个问题,而在Linux上没有。我的测试示例的完整代码:https://pastebin.com/gS7hEXmt - Artem
@Artem。请注意,我说的是:“在大多数PyQt代码中”。你的例子非常人为。当Qt删除一个小部件时,它也会自动删除所有子项。此外,当将小部件添加到布局中时,它将自动重新父级到包含布局的小部件。因此,行编辑器将成为对话框的子项,当删除对话框时,Qt将删除C ++部分。留下Python包装器。如果使用show()打开对话框,则不会发生这种情况,因为对话框不会被删除(其父级将使其保持活动状态)。 - ekhumoro
@ Artem。PS:这与Windows与Linux无关。相同的行为将在两个平台上发生。如果您看到不同的行为,则可能运行不同的示例代码或不同版本的PyQt。 - ekhumoro
代码是一样的,但是我在我的电脑上有不同的版本 - 在Linux上:python 3.4.3,qt/pyqt 5.2.1,在Windows上:python 3.5.4,qt 5.9.2,pyqt 5.9.1。 - Artem
@Artem。pyqt-5.2.1的行为有缺陷。正确的行为是引发一个“RuntimeError”,这也是当前版本的pyqt4和pyqt5在所有平台上都做到的。 - ekhumoro

10

@charley是完全正确的,他很好地解释了理论。尽管在实践中,这种所有权问题可能会在许多情况下发生,但其中最常见的一种是在子类化QT类时忘记调用基类构造函数 - 有时当我从头开始编码时,我总是会卡在这个问题上。

以非常常见的子类化QAbstractTableModel为例:

from PyQt4.QtCore import *


class SomeTableModel(QAbstractTableModel):

  def __init__(self, filename):
    super(SomeTableModel, self).__init__() # If you forget this, you'll get the
                                           # "underlying C/C++ object has been 
                                           # deleted" error when you instantiate
                                           # SomeTableModel.

这与原问题关系不大,更像是对接受答案中的一个单词进行自由联想,针对PySide/PyQt编程有丰富经验的人很少会遇到的问题。在OP中,"对象已被删除"错误不会通过未调用init来明显地出现。 - eric
5
重点不是这种情况不会发生在那些有“PySide/PyQt很多经验”的人身上,而是它确实经常发生,可能只是因为粗心大意或者缺乏经验。而这个网站的目的就是帮助人们解决问题。我相信它比你那些空洞的评论和负评对更多人有所帮助。 - Claudio
2
事实上,他所有的评论只会激怒那些未来几年在寻找这个问题答案的人们... - Glenn Maynard

2

从Python 3+开始,你甚至可以进一步简化代码。

from PyQt4.QtCore import *

class SomeTableModel(QAbstractTableModel):
  def __init__(self, filename):
    super().__init__()   # No longer need to specify class name, or self

此外,Super 还提供了一些额外的保护措施,具体请参见了解 Python super() 和 __init__() 方法


虽然它没有回答我的问题,但这是一个有趣的Py3改进。谢谢。 - Rizo
1
这个回答与问题无关。 - Rafał Łużyński
它原本应该被输入为评论,不确定为什么会是答案。 - Christopher Sean Forgeron
3
啊,现在我想起来为什么这是一个答案了——因为我的声望低于50,所以SO不允许我直接对某些答案进行评论。我又遇到了另一个问题,所以我将另一条“评论”发布为答案。似乎我只能在我的帖子下进行评论。感谢你的投票,让我的声望保持较低,这样我就无法直接发表评论了。 :-) - Christopher Sean Forgeron
这是对一个回答的评论,该回答与问题也没有任何关系。 :) - eric

-2
如何测试底层对象是否存在?如何避免它?
您可以使用:
try: someObject.objectName()
except RuntimeError: return False

.objectName()测试是一个任意的查询,所有有效的QObjects都应该轻松通过。
如果它们失败了,它们将抛出RuntimeError,这表明它们无效。
在这个简单的例子中,我们吸收了RuntimeError,而是返回false。


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