在Python中取消捕获异常

19
我应该如何“重新抛出”异常,即:
  • 我在代码中尝试某些操作,但不幸失败了。
  • 我尝试一些“巧妙”的解决方法,但这次也失败了。
如果我从(失败的)解决方法中抛出异常,那么对于用户来说会非常困惑,所以我认为最好重新抛出原始异常,并附带描述性的回溯信息(关于实际问题)...
注意:这个例子的动机是调用`np.log(np.array(['1'], dtype=object))`时,它尝试一个机智的解决方法并给出`AttributeError`(实际上是`TypeError`)。
我能想到的一种方法就是重新调用有问题的函数,但这似乎很麻烦(理论上,原始函数第二次被调用时可能会表现出不同的行为):
好吧,这是一个非常糟糕的例子,但是还是让我们试试吧...
def f():
    raise Exception("sparrow")

def g():
    raise Exception("coconut")

def a():
    f()

假设我这样做了:
try:
    a()
except:
    # attempt witty workaround
    g()
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-4-c76b7509b315> in <module>()
      3 except:
      4     # attempt witty workaround
----> 5     g()
      6

<ipython-input-2-e641f2f9a7dc> in g()
      4
      5 def g():
----> 6     raise Exception("coconut")
      7
      8

Exception: coconut

好的,问题实际上并不在于椰子,而是麻雀:

try:
    a()
except:
    # attempt witty workaround
    try:
        g()
    except:
        # workaround failed, I want to rethrow the exception from calling a()
        a() # ideally don't want to call a() again
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-4-e641f2f9a7dc> in <module>()
     19     except:
     20         # workaround failed, I want to rethrow the exception from calling a()
---> 21         a()  # ideally don't want to call a() again

<ipython-input-3-e641f2f9a7dc> in a()
      8
      9 def a():
---> 10     f()
     11
     12

<ipython-input-1-e641f2f9a7dc> in f()
      1 def f():
----> 2     raise Exception("sparrow")
      3
      4
      5 def g():

Exception: sparrow

有没有一种标准的方法来处理这个问题,或者我的想法完全错误?


相关问题:https://dev59.com/Cm015IYBdhLWcg3w9gfh - maazza
你尝试过 traceback 模块吗? - kirbyfan64sos
@kirbyfan64sos,你能否用它整理出一个答案? - Andy Hayden
你的直觉是正确的,应该重新引发原始异常:这就是Java try-with-resources(相当于Python中的“with”语句)所做的,但Java还通过Throwable#addSuppressed将次要异常添加为“抑制”异常,因此你实际上可以得到一个异常!请参见谁决定压制哪些异常? - Nils von Barth
7个回答

10

如果您希望终端用户看起来从未调用g(),则需要存储第一个错误的回溯信息,调用第二个函数,然后使用原始的回溯信息将原始异常抛出。(否则,在Python2中,裸的raise会导致第二个异常而不是第一个异常被重新引发)。问题在于没有兼容2/3版本的方法可以带有回溯信息进行异常引发,所以必须将Python 2版本包装在exec语句中(因为在Python 3中它是SyntaxError)。

这里有一个函数可让您执行此操作(最近我将其添加到pandas代码库中):

import sys
if sys.version_info[0] >= 3:
    def raise_with_traceback(exc, traceback=Ellipsis):
        if traceback == Ellipsis:
            _, _, traceback = sys.exc_info()
        raise exc.with_traceback(traceback)
else:
    # this version of raise is a syntax error in Python 3
    exec("""
def raise_with_traceback(exc, traceback=Ellipsis):
    if traceback == Ellipsis:
        _, _, traceback = sys.exc_info()
    raise exc, None, traceback
""")

raise_with_traceback.__doc__ = (
"""Raise exception with existing traceback.
If traceback is not passed, uses sys.exc_info() to get traceback."""
)

然后你可以像这样使用它(我也为了清晰起见更改了异常类型)。

def f():
    raise TypeError("sparrow")

def g():
    raise ValueError("coconut")

def a():
    f()

try:
    a()
except TypeError as e:
    import sys
    # save the traceback from the original exception
    _, _, tb = sys.exc_info()
    try:
        # attempt witty workaround
        g()
    except:
        raise_with_traceback(e, tb)

而在Python 2中,您只会看到a()f()

Traceback (most recent call last):
  File "test.py", line 40, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 31, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

但在Python 3中,它仍然指出有一个额外的异常,因为您正在其except子句中引发异常[这会颠倒错误的顺序,并使用户更加困惑]:

Traceback (most recent call last):
  File "test.py", line 38, in <module>
    g()
  File "test.py", line 25, in g
    raise ValueError("coconut")
ValueError: coconut

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test.py", line 40, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 6, in raise_with_traceback
    raise exc.with_traceback(traceback)
  File "test.py", line 31, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

如果您希望在 Python 2 和 Python 3 中都看起来好像没有发生 g() 异常,那么您需要先检查你是否已经退出了 except 子句:

try:
    a()
except TypeError as e:
    import sys
    # save the traceback from the original exception
    _, _, tb = sys.exc_info()
    handled = False
    try:
        # attempt witty workaround
        g()
        handled = True
    except:
        pass
    if not handled:
        raise_with_traceback(e, tb)

在Python 2中,这会得到以下回溯:

Traceback (most recent call last):
  File "test.py", line 56, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 43, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

而在Python 3中,出现了以下的回溯(traceback):

Traceback (most recent call last):
  File "test.py", line 56, in <module>
    raise_with_traceback(e, tb)
  File "test.py", line 6, in raise_with_traceback
    raise exc.with_traceback(traceback)
  File "test.py", line 43, in <module>
    a()
  File "test.py", line 28, in a
    f()
  File "test.py", line 22, in f
    raise TypeError("sparrow")
TypeError: sparrow

它确实会添加一行额外的无用的回溯信息,显示raise exc.with_traceback(traceback)给用户看,但相对而言还是比较干净的。


1
也许在回答时还没有“没有2/3兼容的方式来引发带有回溯信息的异常”,但现在在six中有一个名为six.reraise的函数,几乎可以做到@jeff在这里展示的所有功能。 - agomcas
你说得对!我不知道这在Six中存在(看起来实际上是在2010年加入库的!)。它的实现方式也非常相似。 - Jeff Tratner

8

这里有一些非常疯狂的东西,我不确定它是否有效,但它在python 2和3中都有效。 (但是,它需要将异常封装到一个函数中...)

def f():
    print ("Fail!")
    raise Exception("sparrow")
def g():
    print ("Workaround fail.")
    raise Exception("coconut")
def a():
    f()

def tryhard():
    ok = False
    try:
        a()
        ok = True
    finally:
        if not ok:
            try:
                g()
                return # "cancels" sparrow Exception by returning from finally
            except:
                pass

>>> tryhard()
Fail!
Workaround fail.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in tryhard
  File "<stdin>", line 2, in a
  File "<stdin>", line 3, in f
Exception: sparrow

哪种正确的异常和正确的堆栈跟踪,而且没有任何hackery。

>>> def g(): print "Worked around." # workaround is successful in this case

>>> tryhard()
Fail!
Worked around.

>>> def f(): print "Success!" # normal method works

>>> tryhard()
Success!

如果你不想将它封装在一个函数中,这个概念也可以通过 finally 中的 break 实现,但是我很惊讶地发现在 finally 中使用 continue 实际上是一个语法错误! - morningstar
当我在函数外尝试使用break时,无论是在Python2还是Python3中,都会出现“SyntaxError: 'break' outside loop”的错误提示。 - Andy Hayden
是的,你需要将整个外部try块放在某种无用的上下文中,例如 for _ in range(1):。我没说它很好看。 - morningstar
1
@morningstar 你认为我使用 finally 是疯狂的,因为我完全忽略了它的本意。但实际上我是按照规范来使用的。+1 - jpaugh
是的,finally子句正是解决此问题的方法:“当在try子句中发生异常并且没有被except子句处理(或者它发生在exceptelse子句中),在执行finally子句后会重新引发该异常。” 教程:定义清理操作,参考资料:try语句 - Nils von Barth

6

Ian Bicking写了一篇关于重新抛出异常的好文章。

作为一个推论,我的规则是只捕获代码知道如何处理的异常。实际上很少有方法符合这个规则。例如,如果我正在读取一个文件并且抛出了IOException,那么该方法可以合理地做很少的事情。

作为这个规则的一个推论,如果您可以返回到一个良好的状态并且不想让用户退出,则在“main”中捕获异常是合理的;这仅适用于交互式程序。

从这篇文章中相关的部分是更新:

try:
    a()
except:
    exc_info = sys.exc_info()
    try:
        g()
    except:
        # If this happens, it clobbers exc_info,
        # which is why we had to save it above
        import traceback
        print >> sys.stderr, "Error in revert_stuff():"
        # py3 print("Error in revert_stuff():", file=sys.stderr)
        traceback.print_exc()
    raise exc_info[0], exc_info[1], exc_info[2]

在Python 3中,最终的raise可以写成
ei = exc_info[1]
ei.filname = exc_info[0]
ei.__traceback__ = exc_info[2]
raise ei

这似乎有点像是一个hack...但这是目前为止唯一可行的答案 :) - Andy Hayden
Ian曾经是这里的一个固定角色;他肯定在战壕中度过了他的时间。我不知道为什么他的SO声望已经崩溃了。 - msw
这种方法(使用保存的exc_info())是在你破坏了先前的异常信息后使事情正常运行的唯一方法。(另请参见https://dev59.com/22oy5IYBdhLWcg3wHqeB)它需要修改以适用于Python 3.x:https://dev59.com/EXDYa4cB1Zd3GeqPB5Qp - torek
@torek 我已经更新了答案,为了方便也包括了那个。 - Andy Hayden
@torek,Python 3不需要保存exc_info。只需使用raise即可。请查看我的回答。 - Mark Tolonen

4
在Python 3中(特别是在3.3.2版本中测试),所有这些功能都更加完善,不需要保存sys.exc_info。在第二个异常处理程序内部不要重新引发原始异常。只需记录第二次尝试失败,并在原始处理程序的范围内引发原始异常,如下所示:
#!python3

try:
    a()
except Exception:
    g_failed = False
    try:
        g()
    except Exception:
        g_failed = True
    raise

Python 3能正确输出并通过a()f()显示"sparrow"的回溯信息:

Traceback (most recent call last):
  File "x3.py", line 13, in <module>
    a()
  File "x3.py", line 10, in a
    f()
  File "x3.py", line 4, in f
    raise Exception("sparrow")
Exception: sparrow

然而,在Python 2上,同样的脚本会错误地引发“coconut”,并且只显示g()

Traceback (most recent call last):
  File "x3.py", line 17, in <module>
    g()
  File "x3.py", line 7, in g
    raise Exception("coconut")
Exception: coconut

以下是使Python 2正常工作的修改方法:
#!python2
import sys

try:
    a()
except Exception:
    exc = sys.exc_info()
    try:
        g()
    except Exception:
        raise exc[0], exc[1], exc[2] # Note doesn't care that it is nested.

现在Python 2已经正确显示了“sparrow”,并且a()f() 都有了回溯信息。
Traceback (most recent call last):
  File "x2.py", line 14, in <module>
    a()
  File "x2.py", line 11, in a
    f()
  File "x2.py", line 5, in f
    raise Exception("sparrow")
Exception: sparrow

啊!这是 Python 2 的问题……又一个需要移植的原因。奇怪。 - Andy Hayden
这是一个不错的想法,如果有一种在Python 2中实现它的方法,那就太棒了! - Andy Hayden
我认为@msw发现的是正确的方法。必须保存sys.exc_info - Mark Tolonen
非常好。只是有点失望它不能同时适用于Python 2和3 :) - Andy Hayden
很棒的答案;可以在一个函数中更加简洁地完成(请查看我的答案)。 - Nils von Barth
显示剩余3条评论

3

在您的except子句中捕获错误,稍后手动重新引发它。使用traceback模块捕获回溯并重新打印它。

import sys
import traceback

def f():
    raise Exception("sparrow")

def g():
    raise Exception("coconut")

def a():
    f()

try:
    print "trying a"
    a()
except Exception as e:
    print sys.exc_info()
    (_,_,tb) = sys.exc_info()
    print "trying g"
    try:
        g()
    except:
        print "\n".join(traceback.format_tb(tb))
        raise e

这仍然引发了一个异常...我不想重新引发最后一个异常,而是之前的那个异常。 - Andy Hayden
1
抱歉,我匆匆看了一下,不知道怎么做。 - jpaugh
哦!自从我上次使用Python以来,出现了一个新的“traceback”模块。这一次我离成功非常接近。试试看吧。 - jpaugh
仍存在与where引起的相同问题,并且它只在traceback中显示一个东西。肯定是traceback模块没错... :( - Andy Hayden
@jpaugh,你只需要使用raise with 3 expressions来自Ian's primer via @msw's answer。此外,您可以使用sys.last_traceback属性而不是转储sys.exc_info()的第1和第2个输出,并使用traceback.print_tb(tb)而不是traceback.format_tb(),但您根本不需要traceback模块。 - Mark Mikofski
显示剩余6条评论

0
在Python 3中,可以使用非常简洁的方式在函数内部完成此操作,参考@Mark Tolonen的答案,他使用了一个布尔值。由于没有办法跳出外部try语句,因此无法在函数外部进行此操作:需要使用函数来执行return
#!python3

def f():
    raise Exception("sparrow")

def g():
    raise Exception("coconut")

def a():
    f()

def h():
    try:
        a()
    except:
        try:
            g()
            return  # Workaround succeeded!
        except:
            pass  # Oh well, that didn't work.
        raise  # Re-raises *first* exception.

h()

这将导致:

Traceback (most recent call last):
  File "uc.py", line 23, in <module>
    h()
  File "uc.py", line 14, in h
    a()
  File "uc.py", line 10, in a
    f()
  File "uc.py", line 4, in f
    raise Exception("sparrow")
Exception: sparrow

...如果相反地g成功:

def g(): pass

...那么它就不会引发异常。


-1
try:
    1/0  # will raise ZeroDivisionError
except Exception as first:
    try:
        x/1  # will raise NameError
    except Exception as second:
        raise first  # will re-raise ZeroDivisionError

1
然后我就失去了剩下的回溯信息(它只返回最后一行,raise 异常的第一行)。 - Andy Hayden
1
我知道这是演示代码,但你永远不想捕获异常,因为它可能是MemoryError,这很难应对,或者是RuntimeError,这通常意味着解释器正在崩溃,你所做的任何事情都可能是错误的。 - msw
@msw 那是我的“错误” :) - Andy Hayden

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接