问题:当槽中出现异常时,由信号调用,它们似乎不会像往常一样通过 Python 调用堆栈传播。在下面调用的示例代码中:
on_raise_without_signal()
:将按预期处理异常。on_raise_with_signal()
:将打印异常,然后意外地打印来自else
block 的成功消息。
问题在插槽中引发异常时意外处理的原因是什么?它是 PySide Qt 信号/插槽包装的一些实现细节/限制吗?文档中有什么值得阅读的内容吗?
PS: 我最初遇到这个话题是因为我在实现 QAbstractTableModels
时使用 try/except/else/finally 得到了惊人的结果虚拟方法 insertRows()
和 removeRows()
。
# -*- coding: utf-8 -*-
"""Testing exception handling in PySide slots."""
from __future__ import unicode_literals, print_function, division
import logging
import sys
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class ExceptionTestWidget(QtGui.QWidget):
raise_exception = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(ExceptionTestWidget, self).__init__(*args, **kwargs)
self.raise_exception.connect(self.slot_raise_exception)
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
# button to invoke handler that handles raised exception as expected
btn_raise_without_signal = QtGui.QPushButton("Raise without signal")
btn_raise_without_signal.clicked.connect(self.on_raise_without_signal)
layout.addWidget(btn_raise_without_signal)
# button to invoke handler that handles raised exception via signal unexpectedly
btn_raise_with_signal = QtGui.QPushButton("Raise with signal")
btn_raise_with_signal.clicked.connect(self.on_raise_with_signal)
layout.addWidget(btn_raise_with_signal)
def slot_raise_exception(self):
raise ValueError("ValueError on purpose")
def on_raise_without_signal(self):
"""Call function that raises exception directly."""
try:
self.slot_raise_exception()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_without_signal() executed successfully")
def on_raise_with_signal(self):
"""Call slot that raises exception via signal."""
try:
self.raise_exception.emit()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_with_signal() executed successfully")
if (__name__ == "__main__"):
application = QtGui.QApplication(sys.argv)
widget = ExceptionTestWidget()
widget.show()
sys.exit(application.exec_())
最佳答案
正如您在问题中已经指出的那样,这里真正的问题是如何处理从 C++ 执行的 python 代码中引发的未处理异常。所以这不仅与信号有关:它还会影响重新实现的虚拟方法。
在 PySide、PyQt4 和所有 5.5 之前的 PyQt5 版本中,默认行为是自动捕获 C++ 端的错误并将回溯转储到 stderr。通常,python 脚本也会在此之后自动终止。但这不是这里发生的事情。相反,PySide/PyQt 脚本不顾一切地继续运行,许多人非常正确地将其视为错误(或至少是错误功能)。在 PyQt-5.5 中,此行为现已更改,因此 qFatal()
也会在 C++ 端调用,程序将像普通 python 脚本一样中止。 (不过我不知道 PySide2 目前的情况如何)。
那么 - 对于这一切应该怎么办?所有版本的 PySide 和 PyQt 的最佳解决方案是安装 exception hook - 因为它总是优先于默认行为(无论是什么)。信号、虚方法或其他 Python 代码引发的任何未处理的异常将首先调用 sys.excepthook
,允许您以任何您喜欢的方式完全自定义行为。
在您的示例脚本中,这可能只是意味着添加如下内容:
def excepthook(cls, exception, traceback):
print('calling excepthook...')
logger.error("{}".format(exception))
sys.excepthook = excepthook
现在可以像处理所有其他未处理的异常一样处理 on_raise_with_signal
引发的异常。
当然,这确实意味着大多数 PySide/PyQt 应用程序的最佳实践是使用主要集中的异常处理。这通常包括显示某种崩溃对话框,用户可以在其中报告意外错误。
关于python - 在 Pyside 插槽中意外处理异常,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45787237/