关于python:在主程序或清理中可能发生错误时的异常处理

Exception handling when errors may occur in main program or in cleanup

这是使用Python2.6.6(默认)的debian-squence。考虑下面的python代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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"

得到的误差是

1
2
3
4
5
6
7
8
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)在《重新提出例外》一书中对此进行了一些讨论。在这篇文章的最后,(见Update:)他描述了如何处理代码+回滚/恢复(仅在出错时运行)的类似情况。

我摆弄了一下,想出了上面的代码,这有点怪。尤其是,如果清除(注释了raise Exception("error in main")),代码仍然正常退出,尽管它确实打印了一个回溯。目前,我将给予非清除错误优先级,因此它将停止程序。

理想情况下,我希望两个错误中的一个停止程序,但这似乎不容易安排。python似乎只想引起一个错误,如果有的话会失去其他错误,默认情况下,它通常是最后一个错误。重新排列会产生像上面那样的卷积。

另外,使用locals()有点难看。一个人能做得更好吗?

编辑:srgerg的回答向我介绍了上下文管理器和with关键字的概念。除PEP 343外,我发现的其他相关文档位(无特定顺序)。上下文管理器类型、WITH语句和http://docs.python.org/reference/datamodel.html上下文管理器。这显然是对以前的方法的一个很大的改进,即涉及trys、excepts和finallys的意大利面代码。

总的来说,有两件事我希望这样的解决方案能给我。

  • 在主代码或清除以使程序停止运行。上下文管理器这样做,因为如果with循环的主体有一个异常并且出口不存在,则会传播该异常。如果exit引发异常而with循环的主体没有,然后传播。如果两者都引发异常,则退出异常被传播,而while循环体中的异常被抑制的所有这些都记录在案,即上下文管理器类型,


    contextmanager.exit(exc_type, exc_val, exc_tb)

    Exit the runtime context and return a Boolean flag indicating if any exception that occurred should be suppressed. [...]
    Returning a true value from this method will cause the with statement to suppress the exception and continue execution with the
    statement immediately following the with statement. Otherwise the exception continues propagating after this method has finished
    executing. Exceptions that occur during execution of this method will replace any exception that occurred in the body of the with
    statement. [...] The exception passed in should never be reraised explicitly. instead, this method should return a false value to
    indicate that the method completed successfully and does not want to suppress the raised exception.

  • 如果这两个地方都有例外,我想看看回溯从这两个方面来看,即使技术上只抛出一个异常。这是基于实验,如果两者都抛出异常,然后传播退出异常,但从主体返回当循环仍在打印时,如斯格格的回答。但是,我找不到这个文件任何地方都不满意。


  • 理想情况下,您可以使用python with语句来处理try ... except块内的清理,该块的外观如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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!"

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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语句中提出一个,可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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!"

    印刷品:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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!


    通过提供定制的异常钩子,可以获得类似的行为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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")

    实例输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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中,我看到了以下实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void
    PyErr_PrintEx(int set_sys_last_vars)
    {
        ...
        hook = PySys_GetObject("excepthook");
        if (hook) {
            ...
            if (result == NULL) {
                ...
                PySys_WriteStderr("Error in sys.excepthook:
    "
    );
                PyErr_Display(exception2, v2, tb2);
                PySys_WriteStderr("
    Original exception was:
    "
    );
                PyErr_Display(exception, v, tb);
                ...
            }
        }
    }


    你非常接近一个简单的解决方案。只需在第一个异常中使用traceback.print_exc(),就不必再处理第二个异常了。以下是可能出现的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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语句的输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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