如何在Python中使用自定义消息引发相同异常?

269

我在代码中有这个try块:

try:
    do_something_that_might_raise_an_exception()
except ValueError as err:
    errmsg = 'My custom error message.'
    raise ValueError(errmsg)

严格来说,我实际上是在引发另一个 ValueError,而不是由do_something...()抛出的ValueError,此时称其为err。我该如何将自定义消息附加到err? 我尝试以下代码但失败了,因为err是一个ValueError 实例,不可调用:

try:
    do_something_that_might_raise_an_exception()
except ValueError as err:
    errmsg = 'My custom error message.'
    raise err(errmsg)

15
@Hamish,当调试时,附加额外信息和重新引发异常可以非常有帮助。 - Johan Lundberg
@Johan 当然没问题 - 这就是堆栈跟踪的作用。我不太明白为什么你要编辑现有的错误消息而不是引发一个新的错误。 - Hamish
@Hamish。当然,你可以添加其他内容。关于你的问题,请看一下我的回答和UnicodeDecodeError的示例。如果你对此有评论,也许可以在我的回答中评论。 - Johan Lundberg
3
@Kit,现在已经是2020年,Python 3 已经普及了。为什么不把采纳的答案改成Ben的答案呢 :-) - mit
1
@mit 现在是2023年,我已经更改了正确答案 :D - Kit
显示剩余2条评论
17个回答

341

如果你很幸运,只需要支持Python 3.x,那么这真的成为一件美妙的事情 :)

raise from

我们可以使用raise from来链接异常。

try:
    1 / 0
except ZeroDivisionError as e:
    raise Exception('Smelly socks') from e

在这种情况下,您的调用者捕获的异常具有引发异常的位置的行号。

Traceback (most recent call last):
  File "test.py", line 2, in <module>
    1 / 0
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "test.py", line 4, in <module>
    raise Exception('Smelly socks') from e
Exception: Smelly socks

请注意,底部的异常只有我们引发异常的堆栈跟踪。调用者仍然可以通过访问他们捕获的异常的__cause__属性来获取原始异常。

或者您可以使用with_traceback

try:
    1 / 0
except ZeroDivisionError as e:
    raise Exception('Smelly socks').with_traceback(e.__traceback__)
使用这个表单,你的调用者捕获的异常将包含原始错误发生位置的回溯信息。
Traceback (most recent call last):
  File "test.py", line 2, in <module>
    1 / 0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test.py", line 4, in <module>
    raise Exception('Smelly socks').with_traceback(e.__traceback__)
  File "test.py", line 2, in <module>
    1 / 0
Exception: Smelly socks

请注意,底部的异常中包含了我们执行无效除法的行以及重新引发异常的行。


6
是否可以在异常中添加自定义消息而不增加其他的回溯信息?例如,是否可以将 raise Exception('臭袜子') from e 修改为只向原始回溯信息中添加“臭袜子”作为注释,而不是引入新的回溯信息。 - joelostblom
这就是你会从Johan Lundberg的回答中得到的行为。 - Ben
7
在许多情况下,重新引发新异常或链式引发带有新消息的异常会造成更多不必要的混淆。本身异常的处理就很复杂。更好的策略是如果可能的话,将您的消息附加到原始异常参数中,例如err.args += ("message",),并重新引发异常消息。回溯可能无法带您到捕获异常的行号,但它一定会带您到异常发生的地方。 - user-asterix
3
您可以通过在 from 子句中指定 None 来显式地禁止显示异常链的 _display_:raise RuntimeError("Something bad happened") from None - pfabri
12
未能回答实际问题。是的,我们都知道如何在2020年链式调用Python 3.x的异常。实际问题是如何修改原始异常的原始异常消息,而不使用链接或其他无关的花哨技巧,这些技巧只会引发新的异常,从而妨碍原始的回溯信息。 - Cecil Curry

126

更新:对于Python 3,请查看Ben的答案

2023年更新:我写这篇答案已经超过十年了,现在有更好的答案。您应该使用Python 3和上面的答案。

原始答案:

要将消息附加到当前异常并重新引发它: (外部try / except仅用于显示效果)

对于Python 2.x,其中x>=6:

try:
    try:
      raise ValueError  # something bad...
    except ValueError as err:
      err.message=err.message+" hello"
      raise              # re-raise current exception
except ValueError as e:
    print(" got error of type "+ str(type(e))+" with message " +e.message)

如果err是从ValueError派生而来的,例如UnicodeDecodeError,那么这也会做正确的事情。

请注意,您可以向err添加任何您喜欢的内容。例如:err.problematic_array=[1,2,3]


编辑:@Ducan在评论中指出,上述代码在Python 3中无法工作,因为.message不是ValueError的成员。相反,您可以使用以下代码(适用于有效的Python 2.6或更高版本或3.x):

try:
    try:
      raise ValueError
    except ValueError as err:
       if not err.args: 
           err.args=('',)
       err.args = err.args + ("hello",)
       raise 
except ValueError as e:
    print(" error was "+ str(type(e))+str(e.args))

编辑2:

根据目的,您还可以选择在自己的变量名称下添加额外信息。 对于Python2和Python3均适用:

try:
    try:
      raise ValueError
    except ValueError as err:
       err.extra_info = "hello"
       raise 
except ValueError as e:
    print(" error was "+ str(type(e))+str(e))
    if 'extra_info' in dir(e):
       print e.extra_info

12
由于您已经使用了 Python 3 风格的异常处理和 print,您应该注意到您的代码在 Python 3.x 中无法工作,因为异常中没有“message”属性。更可靠的方法是err.args = (err.args[0] + " hello",) + err.args[1:](然后将其转换为字符串以获取消息)。请注意不要改变原来的意思。 - Duncan
1
不幸的是,args [0] 不保证是表示错误消息的字符串类型 - “传递给异常构造函数的参数元组。一些内置异常(如IOError)期望一定数量的参数,并将此元组的元素赋予特殊含义,而其他异常通常仅使用单个字符串调用以提供错误消息。”。因此,如果arg [0]不是错误消息(它可能是int,也可能是表示文件名的字符串),则代码将无法工作。 - Trent
1
@Taras,有趣。你有相关的参考资料吗?然后我会添加到一个全新的成员:err.my_own_extra_info。或者将所有内容封装在我的自定义异常中,保留新的和原始的信息。 - Johan Lundberg
3
一个实际的例子说明当 args[0] 不是错误信息时的情况-请参考http://docs.python.org/2/library/exceptions.html-"异常环境错误 这是一个可以在Python系统外发生的异常基类:IOError、OSError等。当创建这种类型的异常时,如果使用一个二元组作为参数,在实例的 errno 属性中第一个项目可用(它被假定为错误编号),第二个项目可用在 strerror 属性中(通常是相关的错误消息)。元组本身也可用在 args 属性中。" - Trent
2
我完全不理解这个。设置.message属性唯一的原因是该属性被显式打印出来。如果你在没有捕获和打印的情况下引发异常,你将看不到.message属性有任何有用的作用。 - DanielSank
显示剩余7条评论

16

这仅适用于Python 3。 您可以修改异常的原始参数并添加自己的参数。

异常会记住创建时使用的args。我认为这是为了您可以修改异常。

在函数reraise中,我们将异常的原始参数与任何想要的新参数(如消息)一起前置。 最后,我们重新引发异常,同时保留跟踪历史记录。

def reraise(e, *args):
  '''re-raise an exception with extra arguments
  :param e: The exception to reraise
  :param args: Extra args to add to the exception
  '''

  # e.args is a tuple of arguments that the exception with instantiated with.
  #
  e.args = args + e.args

  # Recreate the exception and preserve the traceback info so that we can see 
  # where this exception originated.
  #
  raise e.with_traceback(e.__traceback__)   


def bad():
  raise ValueError('bad')

def very():
  try:
    bad()
  except Exception as e:
    reraise(e, 'very')

def very_very():
  try:
    very()
  except Exception as e:
    reraise(e, 'very')

very_very()

输出

Traceback (most recent call last):
  File "main.py", line 35, in <module>
    very_very()
  File "main.py", line 30, in very_very
    reraise(e, 'very')
  File "main.py", line 15, in reraise
    raise e.with_traceback(e.__traceback__)
  File "main.py", line 28, in very_very
    very()
  File "main.py", line 24, in very
    reraise(e, 'very')
  File "main.py", line 15, in reraise
    raise e.with_traceback(e.__traceback__)
  File "main.py", line 22, in very
    bad()
  File "main.py", line 18, in bad
    raise ValueError('bad')
ValueError: ('very', 'very', 'bad')

3
迄今为止最好的答案。这是唯一一个回答了原问题、保留了原始追踪信息并且是纯Python 3.x的答案。同时,感谢您强调了“非常非常糟糕”的梗。幽默无疑是一个好东西——尤其是在像这样干巴巴的技术问题中。太棒了! - Cecil Curry
@CecilCurry 意见不一。我不喜欢这种方式增加了更多的垃圾到回溯中,第15行的框架应该被隐藏起来(如果使用裸的raise而不是一个函数调用进入reraise,它本来就会被隐藏)。奇怪的是使用 e.with_traceback(e.__traceback__)即传递了自己的回溯。通常此方法用于为回溯提供不同的异常实例。最终我认为此 reraise 帮助函数显著地损害了回溯的可读性。请查看 新答案 适用于Python 3.11+。 - wim
@CecilCurry 意见不同。我不喜欢这种方式在回溯中添加更多的垃圾,第15行的那个框架应该被隐藏起来(如果使用裸露的raise而不是调用reraise函数,它本来就会被隐藏)。使用e.with_traceback(e.__traceback__)即传递自己的回溯是很奇怪的。通常这个方法是用来为回溯提供一些不同的异常实例。最终,我认为这个reraise辅助函数严重影响了回溯的可读性。请查看新答案适用于Python 3.11+。 - wim

13
似乎所有的答案都将信息添加到e.args [0],从而改变现有的错误消息。 扩展args元组是否有缺点? 我认为可能的好处是,对于需要解析该字符串的情况,您可以保留原始错误消息;如果您的自定义错误处理生成了多个消息或错误代码,则可以向元组中添加多个元素,以便对通过系统监视工具(如跟踪)解析的情况进行处理。
## Approach #1, if the exception may not be derived from Exception and well-behaved:

def to_int(x):
    try:
        return int(x)
    except Exception as e:
        e.args = (e.args if e.args else tuple()) + ('Custom message',)
        raise

>>> to_int('12')
12

>>> to_int('12 monkeys')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in to_int
ValueError: ("invalid literal for int() with base 10: '12 monkeys'", 'Custom message')
或者
## Approach #2, if the exception is always derived from Exception and well-behaved:

def to_int(x):
    try:
        return int(x)
    except Exception as e:
        e.args += ('Custom message',)
        raise

>>> to_int('12')
12

>>> to_int('12 monkeys')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in to_int
ValueError: ("invalid literal for int() with base 10: '12 monkeys'", 'Custom message')

你认为这种方法有什么不利之处吗?


我的旧答案不会改变 e.args[0]。 - Johan Lundberg

13

Python 3.11+

PEP 678 – Enriching Exceptions with Notes已经被接受并纳入Python 3.11。新的API允许用户将自定义信息附加到现有错误上。当遇到错误时,这对于添加额外的上下文非常有用。

使用add_note方法适合回答最初的问题:

try:
    int("eleven")
except ValueError as e:
    errmsg = "My custom error message."
    e.add_note(errmsg)
    raise

它的呈现效果如下所示:

Traceback (most recent call last):
  File "/tmp/example.py", line 2, in <module>
    int("eleven")
ValueError: invalid literal for int() with base 10: 'eleven'
My custom error message.

Python < 3.11

修改args属性是唯一的方法,它被BaseException.__str__用于渲染异常。你可以扩展args

try:
    int("eleven")
except ValueError as e:
    errmsg = "My custom error message."
    e.args += (errmsg,)
    raise e

这将被呈现为:

Traceback (most recent call last):
  File "/tmp/example.py", line 2, in <module>
    int("eleven")
ValueError: ("invalid literal for int() with base 10: 'eleven'", 'My custom error message.')

或者你可以替换args[0],这有点复杂,但会产生更干净的结果。

try:
    int("eleven")
except ValueError as e:
    errmsg = "My custom error message."
    args = e.args
    if not args:
        arg0 = errmsg
    else:
        arg0 = f"{args[0]}\n{errmsg}"
    e.args = (arg0,) + args[1:]
    raise

这将与Python 3.11+异常__notes__以相同方式呈现:

Traceback (most recent call last):
  File "/tmp/example.py", line 2, in <module>
    int("eleven")
ValueError: invalid literal for int() with base 10: 'eleven'
My custom error message.

'.add_note(...)' 是一个非常优雅的解决方案,适用于3.11及以上版本。 - mehekek

7
try:
    try:
        int('a')
    except ValueError as e:
        raise ValueError('There is a problem: {0}'.format(e))
except ValueError as err:
    print err

打印:

There is a problem: invalid literal for int() with base 10: 'a'

2
我想知道除了再次发出一个实例之外,是否有 Python 惯用语来表示我正在尝试做的事情。 - Kit
@Kit - 我会称之为“重新引发异常”:http://docs.python.org/reference/simple_stmts.html#raise - eumiro
1
@eumiro,你正在创建一个新的异常。看看我的答案。来自你的链接:“……如果要重新引发的异常是当前作用域中最近活动的异常,则应优先使用不带表达式的raise。” - Johan Lundberg
3
raise没有参数时会重新引发异常。如果发帖人想添加消息,他必须引发一个新的异常并可以重用原始异常的消息/类型。 - eumiro
3
如果你想要添加一条消息,你不能通过抛出“ValueError”从头开始创建一个新的消息。这样做会破坏关于它是什么类型的ValueError的基本信息(类似于C++中的切片)。通过使用raise重新抛出相同的异常而不带参数,您可以将具有正确特定类型(派生自ValueError)的原始对象传递。 - Johan Lundberg

6

这个代码模板应该可以让你用自定义的消息抛出一个异常。

try:
     raise ValueError
except ValueError as err:
    raise type(err)("my message")

6
这样做无法保留堆栈跟踪信息。 - plok
1
提问者没有指定要保留堆栈跟踪。 - shrewmouse
1
不要刻意变得愚钝。原始问题是:“如何在Python中使用自定义消息引发相同的异常?” 这个非答案会抛出一个的异常,因此根本没有回答原来的问题。 这就是为什么我们不能拥有好东西的原因。 - Cecil Curry
2
@plok 可以使用 raise type(err)("我的消息") from err - timgeb

4
这是我用来修改Python 2.7和3.x中的异常信息并保留原始回溯的函数。它需要 six 库。
def reraise_modify(caught_exc, append_msg, prepend=False):
    """Append message to exception while preserving attributes.

    Preserves exception class, and exception traceback.

    Note:
        This function needs to be called inside an except because
        `sys.exc_info()` requires the exception context.

    Args:
        caught_exc(Exception): The caught exception object
        append_msg(str): The message to append to the caught exception
        prepend(bool): If True prepend the message to args instead of appending

    Returns:
        None

    Side Effects:
        Re-raises the exception with the preserved data / trace but
        modified message
    """
    ExceptClass = type(caught_exc)
    # Keep old traceback
    traceback = sys.exc_info()[2]
    if not caught_exc.args:
        # If no args, create our own tuple
        arg_list = [append_msg]
    else:
        # Take the last arg
        # If it is a string
        # append your message.
        # Otherwise append it to the
        # arg list(Not as pretty)
        arg_list = list(caught_exc.args[:-1])
        last_arg = caught_exc.args[-1]
        if isinstance(last_arg, str):
            if prepend:
                arg_list.append(append_msg + last_arg)
            else:
                arg_list.append(last_arg + append_msg)
        else:
            arg_list += [last_arg, append_msg]
    caught_exc.args = tuple(arg_list)
    six.reraise(ExceptClass,
                caught_exc,
                traceback)

这是为数不多的几个答案之一,实际上回答了原始问题。所以,这很好。不幸的是,Python 2.7(因此six)已经到了生命周期结束的阶段。所以,这很糟糕。虽然在2020年仍然可以在技术上使用six,但这样做没有任何实质性的好处。现在,纯Python 3.x解决方案更加可取。 - Cecil Curry

3

使用以下方法之一使用您的错误消息引发新异常:

raise Exception('your error message')

或者

raise ValueError('your error message')

在您想提升的位置内,通过使用“from”(仅适用于Python 3.x)将错误消息附加(替换)到当前异常中:
except ValueError as e:
  raise ValueError('your message') from e

谢谢,@gberger,“来自 e”的方法实际上不受 Python 2.x 支持。 - Alexey Antonenko
3
毫不意外,这并没有回答实际问题。 - Cecil Curry

3
尝试以下操作:
try:
    raise ValueError("Original message. ")
except Exception as err:
    message = 'My custom error message. '
    # Change the order below to "(message + str(err),)" if custom message is needed first. 
    err.args = (str(err) + message,)
    raise 

输出:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
      1 try:
----> 2     raise ValueError("Original message")
      3 except Exception as err:
      4     message = 'My custom error message.'
      5     err.args = (str(err) + ". " + message,)

ValueError: Original message. My custom error message.

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