问题:当信号调用插槽中引发异常时,它们似乎不会像往常一样通过 Pythons 调用堆栈传播。在下面的示例代码中调用:
on_raise_without_signal()
:将按预期处理异常。on_raise_with_signal()
:将打印异常,然后意外地从else
块打印成功消息。问题:在插槽中出现异常处理时,出现异常的原因是什么?它是信号/插槽的 PySide Qt包装的一些实现细节/限制吗?在文档中有什么需要阅读的内容吗?
PS:在实施QAbstractTableModels
虚拟方法时,当我使用 try / except / else / finally 获得惊人的结果时,我最初遇到了这个主题{ {1}}和insertRows()
。
removeRows()
答案 0 :(得分:3)
正如您在问题中已经注意到的那样,这里真正的问题是处理从C ++执行的python代码中引发的未处理异常。所以这不仅仅是关于信号:它还会影响重新实现的虚拟方法。
在PySide,PyQt4和所有高达5.5的PyQt5版本中,默认行为是自动捕获C ++端的错误并将回溯转储到stderr。通常,python脚本也会在此之后自动终止。但这不是这里发生的事情。相反,PySide / PyQt脚本无论如何都会继续下去,很多人都认为这是一个bug(或者至少是错误的)。在PyQt-5.5中,这种行为现在已经改变,因此在C ++端也调用qFatal()
,程序将像普通的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应用程序的最佳实践是使用大部分集中的异常处理。这通常包括显示某种崩溃对话框,用户可以在其中报告意外错误。
答案 1 :(得分:1)
根据Qt5 docs,您需要处理被调用的插槽中的异常。
从Qt的信号槽连接机制调用的槽中抛出异常被认为是未定义的行为,除非它在槽内处理
State state;
StateListener stateListener;
// OK; the exception is handled before it leaves the slot.
QObject::connect(&state, SIGNAL(stateChanged()), &stateListener, SLOT(throwHandledException()));
// Undefined behaviour; upon invocation of the slot, the exception will be propagated to the
// point of emission, unwinding the stack of the Qt code (which is not guaranteed to be exception safe).
QObject::connect(&state, SIGNAL(stateChanged()), &stateListener, SLOT(throwUnhandledException()));
如果直接调用插槽,就像常规函数调用一样, 可以使用例外。这是因为连接机制是 直接调用插槽时绕过
在第一种情况下,您直接致电slot_raise_exception()
,所以这很好。
在第二种情况下,您通过raise_exception
信号调用它,因此异常只会传播到调用slot_raise_exception()
的位置。您需要将try/except/else
置于slot_raise_exception()
内以便正确处理异常。
答案 2 :(得分:0)
感谢您回答。我发现 ekhumoros 答案对于了解处理异常的位置以及利用sys.excepthook
的想法特别有用。
我通过上下文管理器模拟了一个快速解决方案,暂时扩展当前sys.excepthook
以记录" C ++调用Python" 领域的任何异常(因为它似乎是在信号或虚方法调用插槽时发生的)并且可能在退出上下文时重新加注以在 try / except / else / finally 块中实现预期的控制流。
上下文管理器允许on_raise_with_signal
与周围的 try / except / else / finally 块保持与on_raise_without_signal
相同的控制流。
# -*- coding: utf-8 -*-
"""Testing exception handling in PySide slots."""
from __future__ import unicode_literals, print_function, division
import logging
import sys
from functools import wraps
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class ExceptionHook(object):
def extend_exception_hook(self, exception_hook):
"""Decorate sys.excepthook to store a record on the context manager
instance that might be used upon leaving the context.
"""
@wraps(exception_hook)
def wrapped_exception_hook(exc_type, exc_val, exc_tb):
self.exc_val = exc_val
return exception_hook(exc_type, exc_val, exc_tb)
return wrapped_exception_hook
def __enter__(self):
"""Temporary extend current exception hook."""
self.current_exception_hook = sys.excepthook
sys.excepthook = self.extend_exception_hook(sys.excepthook)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Reset current exception hook and re-raise in Python call stack after
we have left the realm of `C++ calling Python`.
"""
sys.excepthook = self.current_exception_hook
try:
exception_type = type(self.exc_val)
except AttributeError:
pass
else:
msg = "{}".format(self.exc_val)
raise exception_type(msg)
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:
with ExceptionHook() as exception_hook:
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_())
答案 3 :(得分:0)
考虑到信号/插槽体系结构提出了信号和插槽之间的松耦合耦合,因此处理异常的这种方式不足为奇。这意味着信号不应期望插槽内发生任何事情。
尽管timmwagener的解决方案非常聪明,但应谨慎使用。问题可能不在于如何处理Qt Connections之间的异常,而是信号/插槽体系结构不是您的应用程序的理想选择。另外,如果连接了来自其他线程的插槽,或者使用了Qt.QueuedConnection,则该解决方案将不起作用。
解决插槽中出现的错误问题的一种好方法是确定连接处而不是发射处。然后可以以松散耦合的方式处理错误。
class ExceptionTestWidget(QtGui.QWidget):
error = QtCore.Signal(object)
def abort_execution():
pass
def error_handler(self, err):
self.error.emit(error)
self.abort_execution()
(...)
def connect_with_async_error_handler(sig, slot, error_handler, *args,
conn_type=None, **kwargs):
@functools.wraps(slot)
def slot_with_error_handler(*args):
try:
slot(*args)
except Exception as err:
error_handler(err)
if conn_type is not None:
sig.connect(slot_with_error_handler, conn_type)
else:
sig.connect(slot_with_error_handler)
这样,我们将符合Qt5 docs中的要求,表明您需要在被调用的插槽内处理异常。
从Qt的信号插槽调用的插槽引发异常 连接机制被认为是未定义的行为,除非它是 在插槽中处理
PS:这仅是基于很小的用例概述的建议。 没有正确/错误的解决方法,我只是想提出一种不同的观点:)