代码之家  ›  专栏  ›  技术社区  ›  timmwagener

Pyside插槽中意外处理的异常

  •  6
  • timmwagener  · 技术社区  · 8 年前

    当异常在插槽中引发并由信号调用时,它们似乎不会像往常一样通过 蟒蛇 调用堆栈。在下面的示例代码中,调用:

    • on_raise_without_signal()
    • on_raise_with_signal() :将打印异常,然后意外打印来自 else

    在插槽中引发异常时意外处理异常的原因是什么?是否有一些实施细节/限制 信号/插槽的Qt包装?文件里有什么值得一读的吗?

    PS: try/except/else/finally QAbstractTableModels 虚拟方法 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_())
    
    4 回复  |  直到 4 年前
        1
  •  11
  •   ekhumoro    8 年前

    正如您在问题中已经指出的那样,这里真正的问题是如何处理从C++执行的python代码中引发的未处理异常。所以这不仅仅是关于信号:它还影响重新实现的虚拟方法。

    在PySide、PyQt4以及5.5之前的所有PyQt5版本中,默认行为是自动捕获C++端的错误,并将回溯转储到stderr。通常,python脚本在此之后也会自动终止。但这不是这里发生的事情。相反,PySide/PyQt脚本无论如何都会继续运行,许多人非常正确地认为这是一个bug(或者至少是一个错误特性)。在PyQt-5.5中,这种行为现在已经改变,因此 qFatal()

    那么,我们应该怎么做呢?对于所有版本的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 可以用与所有其他未处理的异常相同的方式处理。

        2
  •  3
  •   user3419537    8 年前

    根据 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\u raise\u异常() 被称为。您需要放置 try/except/else slot\u raise\u异常() 以正确处理异常。

        3
  •  0
  •   timmwagener    8 年前

    谢谢你们的回答。我找到了 答案对于理解异常处理的位置以及利用的想法特别有用 sys.excepthook

    我通过上下文管理器模拟了一个快速解决方案,以临时扩展当前 系统。例外钩 记录以下领域中的任何异常: (当信号或虚拟方法调用时隙时,似乎会发生这种情况),并可能在退出上下文时重新引发,以在中实现预期的控制流 阻碍。

    on_raise_with_signal 保持与相同的控制流 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_())
    
        4
  •  0
  •   Nicolas Ryberg    4 年前

    考虑到信号/时隙架构提出了信号和时隙之间的松耦合交互,这种处理表达式的方法并不奇怪。这意味着信号不应期望插槽内发生任何事情。

    timmwagener 的解决方案相当聪明,应该谨慎使用。问题可能不在于如何处理Qt连接之间的异常,而在于信号/插槽架构并不适合您的应用程序。此外,如果连接了来自不同线程的插槽或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的信号槽调用的槽引发异常 连接机制被视为未定义的行为,除非它是 在插槽内处理

    这只是一个基于用例的非常小的概述的建议。 解决这个问题没有正确/错误的方法 ,我只是想提出一个不同的观点:)

    推荐文章