【问题标题】:Exception handling when errors may occur in main program or in cleanup主程序或清理中可能发生错误时的异常处理
【发布时间】:2012-01-02 08:23:40
【问题描述】:

这是 Debian Squeeze 上的 Python 2.6.6(默认)。考虑以下 Python 代码。

import sys
try:
    raise Exception("error in main")
    pass
except:
    exc_info = sys.exc_info()
finally:
    try:
        print "cleanup - always run"
        raise Exception("error in cleanup")
    except:
        import traceback
        print >> sys.stderr, "Error in cleanup"
        traceback.print_exc()
    if 'exc_info' in locals():
        raise exc_info[0], exc_info[1], exc_info[2]

print "exited normally"

得到的错误是

Error in cleanup
Traceback (most recent call last):
  File "<stdin>", line 10, in <module>
Exception: error in cleanup
cleanup - always run
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
Exception: error in main

这个想法是为了应对某些代码或该代码的清理(始终运行)或两者都给出错误的情况。例如,Ian Bicking 在Re-raising Exceptions 中对此进行了一些讨论。在那篇文章的最后,(见Update:)他描述了如何处理代码+回滚/还原的类似情况(仅在出现错误的情况下运行)。

我摆弄了这个并想出了上面的代码,这有点怪异。特别是,如果 only 出现错误 清理(​​注释掉raise Exception("error in main")),代码仍然正常退出,尽管它确实打印出回溯。目前,我给予非清理错误优先级,所以它可以停止程序。

理想情况下,我希望任何一个错误都可以停止程序,但这似乎并不容易安排。 Python 似乎只想引发一个错误,如果有其他错误,则丢失其他错误,默认情况下它通常是最后一个错误。重新排列会产生类似上面的卷积。

另外locals() 的使用有点难看。可以做得更好吗?

编辑:srgerg's answer 向我介绍了上下文管理器和 with 关键字的概念。除了PEP 343,我发现的其他相关文档是(不分先后)。 Context Manager TypesThe with statementhttp://docs.python.org/reference/datamodel.html#context-managers。与以前的方法相比,这似乎是一个很大的改进,即涉及 try、excepts 和 finallys 的意大利面条代码。

总而言之,我希望这样的解决方案能给我两件事。

  1. 主代码或 清理以停止程序的运行。上下文管理器这样做, 因为如果 with 循环的主体有异常并且 exit 没有,则传播该异常。 如果 exit 抛出异常而 with 循环体没有, 然后传播。如果两者都抛出异常,则 exit 传播异常,while循环体中的异常是 压制。这一切都记录在案,即来自 Context Manager Types,

    contextmanager.退出(exc_type、exc_val、exc_tb)

    退出运行时上下文并返回一个布尔标志,指示是否应抑制发生的任何异常。 [...] 从此方法返回真值将导致 with 语句抑制异常并继续执行 紧跟 with 语句的语句。否则,此方法完成后异常继续传播 执行。执行此方法期间发生的异常将替换 with
    正文中发生的任何异常 陈述。 [...] 传入的异常永远不应该被明确地重新引发。相反,这个方法应该返回一个假值 表明该方法成功完成并且不想抑制引发的异常。

  2. 如果两个地方都有异常,我想看回溯 来自两者,即使技术上只抛出一个异常。这是 true 基于实验,因为如果两者都抛出异常, 然后传播 exit 异常,但从主体回溯 仍然打印 while 循环,如 srgerg's answer。 但是,我找不到这个记录 任何地方,都不尽如人意。

【问题讨论】:

    标签: python exception-handling


    【解决方案1】:

    理想情况下,您应该使用 python with statement 来处理 try ... except 块内的清理工作,如下所示:

    class Something(object):
        def __enter__(self):
            print "Entering"
    
        def __exit__(self, t, v, tr):
            print "cleanup - always runs"
            raise Exception("Exception occurred during __exit__")
    
    try:
        with Something() as something:
            raise Exception("Exception occurred!")
    except Exception, e:
        print e
        import traceback
        traceback.print_exc(e)
    
    print "Exited normally!"
    

    当我运行它时,它会打印:

    Entering
    cleanup - always runs
    Exception occurred during __exit__
    Traceback (most recent call last):
      File "s3.py", line 11, in <module>
        raise Exception("Exception occurred!")
      File "s3.py", line 7, in __exit__
        raise Exception("Exception occurred during __exit__")
    Exception: Exception occurred during __exit__
    Exited normally!
    

    注意,任一异常都会终止程序,可以在except 语句中处理。

    编辑:根据上面链接的 with 语句文档,__exit__() 方法应该只在 __exit__() 内部出现错误时引发异常 - 也就是说,它不应该重新 -引发传递给它的异常。

    如果with 语句和__exit__() 方法中的代码都引发异常,则会出现问题。在这种情况下,在 except 子句中捕获的异常是在 __exit__() 中引发的异常。如果你想要在 with 语句中提出的那个,你可以这样做:

    class Something(object):
        def __enter__(self):
            print "Entering"
    
        def __exit__(self, t, v, tr):
            print "cleanup - always runs"
            try:
                raise Exception("Exception occurred during __exit__")
            except Exception, e:
                if (t, v, tr) != (None, None, None):
                    # __exit__ called with an existing exception
                    return False
                else:
                    # __exit__ called with NO existing exception
                    raise
    
    try:
        with Something() as something:
            raise Exception("Exception occurred!")
            pass
    except Exception, e:
        print e
        traceback.print_exc(e)
        raise
    
    print "Exited normally!"
    

    打印出来:

    Entering
    cleanup - always runs
    Exception occurred!
    Traceback (most recent call last):
      File "s2.py", line 22, in <module>
        raise Exception("Exception occurred!")
    Exception: Exception occurred!
    Traceback (most recent call last):
      File "s2.py", line 22, in <module>
       raise Exception("Exception occurred!")
    Exception: Exception occurred!
    

    【讨论】:

    • 谢谢,这对我来说是一个全新的想法。有清理raise Exception("Exception occurred during __exit__") 的想法吗?如果是这样,您可以在此处添加适当的print "cleanup - always run" 或类似名称吗?
    • 我已按要求添加了打印语句。
    • 谢谢,srgerg。我正在阅读 PEP。我想我以前没有听说过或见过with 关键字,但也许我只是没有注意。将清理代码放在进入或退出函数中,还是没有区别?
    • 它确实有所作为。清理代码应该进入 exit
    • 好的,谢谢。不过,您原始代码的输出在我看来仍然是错误的。请确认。另外,我认为docs.python.org/library/stdtypes.html#context-manager-types 的文档很有用。
    【解决方案2】:

    通过提供自定义的exception hook,可以获得类似的行为:

    import sys, traceback
    
    def excepthook(*exc_info):
        print "cleanup - always run"
        raise Exception("error in cleanup")
        traceback.print_exception(*exc_info)
    sys.excepthook = excepthook
    
    raise Exception("error in main")
    

    示例输出:

    cleanup - always run
    Error in sys.excepthook:
    Traceback (most recent call last):
      File "test.py", line 5, in excepthook
        raise Exception("error in cleanup")
    Exception: error in cleanup
    
    Original exception was:
    Traceback (most recent call last):
      File "test.py", line 9, in <module>
        raise Exception("error in main")
    Exception: error in main
    

    在此示例中,代码的工作方式如下:

    • 如果未捕获到异常,则执行excepthook
    • 在打印异常之前,excepthook 会运行一些清理代码(在原始问题中位于 finally 下)。
    • 如果在挂钩中引发异常,则会打印该异常,然后还会打印原始异常。

    注意:我还没有找到任何有关在钩子中发生故障时打印原始异常的文档,但是我在 cpython 和 jython 中都看到了这种行为。特别是,在 cpython 中,我看到了以下实现:

    void
    PyErr_PrintEx(int set_sys_last_vars)
    {
        ...
        hook = PySys_GetObject("excepthook");
        if (hook) {
            ...
            if (result == NULL) {
                ...
                PySys_WriteStderr("Error in sys.excepthook:\n");
                PyErr_Display(exception2, v2, tb2);
                PySys_WriteStderr("\nOriginal exception was:\n");
                PyErr_Display(exception, v, tb);
                ...
            }
        }
    }
    

    【讨论】:

    • 谢谢,但我不清楚这将如何在我的脚本上下文中工作。你能举一个类似的例子,或者同样的例子吗?
    • @FaheemMitha 我已经编辑了答案以添加更多信息。如果我理解正确,你的问题和我的回答中的例子实际上是一样的。
    • 谢谢,您的解释有助于澄清事情。似乎对范围内的所有异常都运行了异常钩子,对吗?如何将其本地化到单个特定上下文?我不希望它在所有情况下都运行。
    • 对所有未捕获的异常执行Exceptionhook。如果您仅在某些地方需要此功能,您可以将sys.excepthook 恢复为原始值(在sys.__excepthook__ 中可用)。
    • 啊,我明白了,所以需要将其从默认值重写,然后在预期使用后恢复为默认值。
    【解决方案3】:

    您非常接近一个简单的解决方案。只需在第一个异常中使用 traceback.print_exc() - 然后您就不必再处理第二个异常了。这可能是这样的:

    error7 = False
    try:
        raise Exception("error in main")
        pass
    except:
        import traceback
        traceback.print_exc()
        error7 = True
    finally:
        print "cleanup - always run"
        raise Exception("error in cleanup")
        if error7:
            raise SystemExit()
    
    print "exited normally"
    

    是否引发异常的信息存储在error7 中,如果是这种情况,则在finally 块的末尾引发SystemExit()

    同时启用raise 语句的输出:

    cleanup - always run
    Traceback (most recent call last):
      File "G:/backed-up to mozy/Scripts/sandbox.py", line 3, in <module>
        raise Exception("error in main")
    Exception: error in main
    Traceback (most recent call last):
    
      File "<ipython-input-1-10089b43dd14>", line 1, in <module>
        runfile('G:/backed-up to mozy/Scripts/sandbox.py', wdir='G:/backed-up to mozy/Scripts')
    
      File "C:\Anaconda2\lib\site-packages\spyder\utils\site\sitecustomize.py", line 866, in runfile
        execfile(filename, namespace)
    
      File "C:\Anaconda2\lib\site-packages\spyder\utils\site\sitecustomize.py", line 87, in execfile
        exec(compile(scripttext, filename, 'exec'), glob, loc)
    
      File "G:/backed-up to mozy/Scripts/sandbox.py", line 11, in <module>
        raise Exception("error in cleanup")
    
    Exception: error in cleanup
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-01-20
      • 1970-01-01
      • 2015-12-26
      • 1970-01-01
      • 1970-01-01
      • 2015-03-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多