【问题标题】:"Uncatching" an exception in pythonpython中的“未捕获”异常
【发布时间】:2013-06-04 18:52:00
【问题描述】:

我应该如何“重新抛出”异常,即假设:

  • 我在我的代码中尝试了一些东西,但不幸的是它失败了。
  • 我尝试了一些“聪明”的解决方法,但这次也失败了

如果我从(失败的)解决方法中抛出异常,用户会非常困惑,所以我认为最好重新抛出原始异常 (?),带有描述性回溯(关于实际问题)...

注意:这方面的激励示例是在调用np.log(np.array(['1'], dtype=object)) 时,它是tries a witty workaround and gives an 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

有没有标准的方法来处理这个问题,还是我认为它完全错误?

【问题讨论】:

  • 你试过traceback模块吗?
  • @kirbyfan64sos 是否愿意使用它来汇总答案?
  • 您认为应该重新引发原始异常的直觉是正确的:这就是在 Java try-with-resources 中所做的(相当于 Python with 语句),但 Java 还添加了次要异常作为通过Throwable#addSuppressed 的“抑制”异常,因此您实际上可以获得tree 异常!见Who decides what exceptions get suppressed?

标签: python exception exception-handling


【解决方案1】:

如果您想让最终用户觉得您从未调用过g(),那么您需要存储第一个错误的回溯,调用第二个函数,然后将原始回溯与原始回溯一起抛出。 (否则,在 Python2 中,bare 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 (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),但它相对干净。

【讨论】:

  • 也许在写这个答案的时候有'没有 2/3 兼容的方式来提高回溯'但是现在在six 有一个函数 Six.reraise 几乎可以做@jeff显示在这里。
  • 你是对的!我不知道那是六个(看起来它实际上是在 2010 年添加到库中的!)。它也以非常相似的方式实现
【解决方案2】:

这是一个完全疯狂的东西,我不确定它是否会起作用,但它在 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

哪个是正确的异常和正确的堆栈跟踪,并且没有hacky。

>>> 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 实际上是一种语法错误!
  • 嗯,我在函数外尝试时得到SyntaxError: 'break' outside loop? (py2和3)
  • 是的,您需要将整个外部 try 块包含在某种无偿上下文中,例如 for _ in range(1):。我没说它漂亮。
  • @morningstar 你说你使用finally 本来就是这么疯狂的......而我完全忽略了它。 +1
  • 是的,finally 子句正是这个问题的解决方案:“当try 子句中发生异常并且未被except 子句处理时(或者它已经发生在exceptelse 子句中),在finally 子句执行后重新引发。”教程:Defining Clean-up Actions,参考:The try statement
【解决方案3】:

Ian Bicking 有一个很好的primer on re-raising

作为推论,我的规则是只捕获代码知道如何处理的异常。很少有方法真正符合这条规则。例如,如果我正在读取一个文件并抛出一个 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 中,final raise could be written as:

ei = exc_info[1]
ei.filname = exc_info[0]
ei.__traceback__ = exc_info[2]
raise ei

【讨论】:

  • 这似乎有点像黑客......但这是迄今为止唯一有效的答案:)
  • Ian 曾经是这里的固定装置;他当然是在战壕里度过的。我不知道他的 SO 代表为什么崩溃了。
  • 这个方法(使用保存的exc_info())是在你破坏了之前的异常信息之后使事情正常工作的唯一方法。 (另请参阅stackoverflow.com/questions/8760267/…)它需要针对 Python 3.x 进行修改:stackoverflow.com/questions/15838224/…
  • @torek 为了方便起见,我已经更新了答案以包含它。
  • @torek,Python 3 不需要保存 exc_info。只需raise。看我的回答。
【解决方案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 输出正确地提升了“sparrow”并通过a()f() 显示回溯:

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 正确显示“麻雀”以及 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 3 中效果更好。您的代码只需一个简单的raise 也可以。无需保存exc_info
  • 啊!这是一个 python 2 的东西......移植的另一个原因。很奇怪。
  • 是个好主意,如果知道在 python 2 中是否有这样的方法会很酷!
  • 我认为@msw 找到的就是方法。 sys.exc_info 必须保存。
【解决方案5】:

捕获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

【讨论】:

  • 这仍然引发了一个椰子......我不想重新提出最后一个例外,但之前的那个。 ?
  • 抱歉,我略读了。我不知道该怎么做。
  • 也许您可以保存异常(通过except)并直接重新引发它,然后呢?
  • 在这里,我重写了我的答案。
  • 只显示异常的最后一行,即你丢失了回溯,并且指向raise e。 :(
【解决方案6】:

在 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

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

【讨论】:

    【解决方案7】:
    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
    

    【讨论】:

    • 然后我丢失了其余的回溯(它只返回最后一行,先加注)。
    • 我知道这是演示代码,但你永远不想捕获异常,因为它可能是一个很难处理的 MemoryError 或 RuntimeError 这主要意味着解释器正在死去,而你所做的任何事情可能是错误的。
    • @msw 这是我的“错误”:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-08
    • 2010-09-28
    • 2012-05-31
    相关资源
    最近更新 更多