Python 中 "raise from" 的用法

443 投票
3 回答
192835 浏览
提问于 2025-04-18 13:22

在Python中,raiseraise from有什么区别呢?

try:
    raise ValueError
except Exception as e:
    raise IndexError

这会产生

Traceback (most recent call last):
  File "tmp.py", line 2, in <module>
    raise ValueError
ValueError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "tmp.py", line 4, in <module>
    raise IndexError
IndexError

try:
    raise ValueError
except Exception as e:
    raise IndexError from e

这会产生

Traceback (most recent call last):
  File "tmp.py", line 2, in <module>
    raise ValueError
ValueError

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

Traceback (most recent call last):
  File "tmp.py", line 4, in <module>
    raise IndexError from e
IndexError

3 个回答

5

最简单的回答PEP-3134 说得很清楚。使用 raise Exception from e 可以把新异常的 __cause__ 字段设置为原来的异常。

更详细的回答来自同一个PEP:

  • __context__ 字段会在 except: 块中自动设置为原始错误,除非你特别指定 __suppress_context__ = True 来不显示这个上下文。
  • __cause__ 和上下文类似,但需要你用 from 语法来明确设置。
  • 当你在 except 块中调用 raise 时,traceback 会自动链接。你可以通过 a) 忽略异常 except: pass 或者直接操作 sys.exc_info() 来去掉 traceback。

详细的回答

import traceback 
import sys

class CustomError(Exception):
    def __init__(self):
        super().__init__("custom")

def print_exception(func):
    print(f"\n\n\nEXECURTING FUNCTION '{func.__name__}' \n")
    try:
        func()
    except Exception as e:
        "Here is result of our actions:"
        print(f"\tException type:    '{type(e)}'")
        print(f"\tException message: '{e}'")
        print(f"\tException context: '{e.__context__}'")
        print(f"\tContext type:      '{type(e.__context__)}'")
        print(f"\tException cause:   '{e.__cause__}'")
        print(f"\tCause type:         '{type(e.__cause__)}'")
        print("\nTRACEBACKSTART>>>")
        traceback.print_exc()
        print("<<<TRACEBACKEND")


def original_error_emitter():
    x = {}
    print(x.does_not_exist)

def vanilla_catch_swallow():
    """Nothing is expected to happen"""
    try:
        original_error_emitter()
    except Exception as e:
        pass

def vanilla_catch_reraise():
    """Nothing is expected to happen"""
    try:
        original_error_emitter()
    except Exception as e:
        raise e

def catch_replace():
    """Nothing is expected to happen"""
    try:
        original_error_emitter()
    except Exception as e:
        raise CustomError()

def catch_replace_with_from():
    """Nothing is expected to happen"""
    try:
        original_error_emitter()
    except Exception as e:
        raise CustomError() from e

def catch_reset_trace():
    saw_an_error = False
    try:
        original_error_emitter()
    except Exception as e:
        saw_an_error = True
    if saw_an_error:
        raise CustomError()

print("Note: This will print nothing")
print_exception(vanilla_catch_swallow)
print("Note: This will print AttributeError and 1 stack trace")
print_exception(vanilla_catch_reraise)
print("Note: This will print CustomError with no context but 2 stack traces")
print_exception(catch_replace)
print("Note: This will print CustomError with AttributeError context and 2 stack traces")
print_exception(catch_replace_with_from)
print("Note: This will brake traceback chain")
print_exception(catch_reset_trace)

将会产生以下输出:

Note: This will print nothing
EXECURTING FUNCTION 'vanilla_catch_swallow' 




Note: This will print AttributeError and 1 stack trace
EXECURTING FUNCTION 'vanilla_catch_reraise' 

        Exception type:    '<class 'AttributeError'>'
        Exception message: ''dict' object has no attribute 'does_not_exist''
        Exception context: 'None'
        Context type:      '<class 'NoneType'>'
        Exception cause:   'None'
        Cause type:         '<class 'NoneType'>'

TRACEBACKSTART>>>
Traceback (most recent call last):
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
    func()
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 41, in vanilla_catch_reraise
    raise e
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 39, in vanilla_catch_reraise
    original_error_emitter()
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
    print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'
<<<TRACEBACKEND



Note: This will print CustomError with no context but 2 stack traces
EXECURTING FUNCTION 'catch_replace' 

        Exception type:    '<class '__main__.CustomError'>'
        Exception message: 'custom'
        Exception context: ''dict' object has no attribute 'does_not_exist''
        Context type:      '<class 'AttributeError'>'
        Exception cause:   'None'
        Cause type:         '<class 'NoneType'>'

TRACEBACKSTART>>>
Traceback (most recent call last):
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 46, in catch_replace
    original_error_emitter()
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
    print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
    func()
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 48, in catch_replace
    raise CustomError()
CustomError: custom
<<<TRACEBACKEND



Note: This will print CustomError with AttributeError context and 2 stack traces
EXECURTING FUNCTION 'catch_replace_with_from' 

        Exception type:    '<class '__main__.CustomError'>'
        Exception message: 'custom'
        Exception context: ''dict' object has no attribute 'does_not_exist''
        Context type:      '<class 'AttributeError'>'
        Exception cause:   ''dict' object has no attribute 'does_not_exist''
        Cause type:         '<class 'AttributeError'>'

TRACEBACKSTART>>>
Traceback (most recent call last):
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 53, in catch_replace_with_from
    original_error_emitter()
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 27, in original_error_emitter
    print(x.does_not_exist)
AttributeError: 'dict' object has no attribute 'does_not_exist'

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

Traceback (most recent call last):
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
    func()
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 55, in catch_replace_with_from
    raise CustomError() from e
CustomError: custom
<<<TRACEBACKEND



Note: This will brake traceback chain
EXECURTING FUNCTION 'catch_reset_trace' 

        Exception type:    '<class '__main__.CustomError'>'
        Exception message: 'custom'
        Exception context: 'None'
        Context type:      '<class 'NoneType'>'
        Exception cause:   'None'
        Cause type:         '<class 'NoneType'>'

TRACEBACKSTART>>>
Traceback (most recent call last):
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 11, in print_exception
    func()
  File "/Users/eugene.selivonchyk/repo/experiments/exceptions.py", line 64, in catch_reset_trace
    raise CustomError()
CustomError: custom
<<<TRACEBACKEND
20

在2005年,PEP 3134,异常链和嵌入式追踪引入了异常链的概念:

  • 隐式链,通过显式的 raise EXCEPTION 或隐式的抛出(__context__ 属性);
  • 显式链,通过显式的 raise EXCEPTION from CAUSE__cause__ 属性)。

动机

在处理一个异常(异常A)时,可能会发生另一个异常(异常B)。在现在的Python(2.4版本)中,如果发生这种情况,异常B会被抛出,而异常A则会消失。为了调试问题,了解这两个异常是很有用的。__context__ 属性会自动保留这些信息。

有时候,异常处理程序可能需要故意重新抛出一个异常,可能是为了提供额外的信息,或者将一个异常转换为另一种类型。__cause__ 属性提供了一种明确的方式来记录异常的直接原因。

[…]

隐式异常链

下面是一个例子来说明 __context__ 属性:

def compute(a, b):
    try:
        a/b
    except Exception, exc:
        log(exc)

def log(exc):
    file = open('logfile.txt')  # oops, forgot the 'w'
    print >>file, exc
    file.close()

调用 compute(0, 0) 会导致一个 ZeroDivisionErrorcompute() 函数捕获了这个异常并调用 log(exc),但 log() 函数在尝试写入一个未打开的文件时也会抛出异常。

在现在的Python中,调用 compute() 的地方会抛出一个 IOError。而 ZeroDivisionError 就会消失。通过提议的更改,IOError 的实例会有一个额外的 __context__ 属性,保留了 ZeroDivisionError

[…]

显式异常链

异常对象上的 __cause__ 属性总是初始化为 None。它通过一种新的 raise 语句形式来设置:

raise EXCEPTION from CAUSE

这等同于:

exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc

在下面的例子中,一个数据库提供了几种不同类型的存储实现,其中文件存储是一种。数据库设计者希望错误以 DatabaseError 对象的形式传播,这样客户端就不需要了解存储的具体细节,但又不想丢失底层的错误信息。

class DatabaseError(Exception):
    pass

class FileDatabase(Database):
    def __init__(self, filename):
        try:
            self.file = open(filename)
        except IOError, exc:
            raise DatabaseError('failed to open') from exc

如果调用 open() 抛出异常,问题将作为 DatabaseError 报告,并且 __cause__ 属性会显示 IOError 作为原始原因。

增强报告

默认的异常处理程序将被修改,以报告链式异常。通过跟踪 __cause____context__ 属性来遍历异常链,其中 __cause__ 优先。根据追踪的时间顺序,最近抛出的异常会最后显示;也就是说,显示从最内层的异常描述开始,逐步回溯到最外层的异常。追踪的格式与往常一样,其中一行是:

上述异常是以下异常的直接原因:

或者

在处理上述异常时,发生了另一个异常:

根据它们是通过 __cause__ 还是 __context__ 连接的,追踪之间会有不同的描述。下面是这个过程的简要说明:

def print_chain(exc):
    if exc.__cause__:
        print_chain(exc.__cause__)
        print '\nThe above exception was the direct cause...'
    elif exc.__context__:
        print_chain(exc.__context__)
        print '\nDuring handling of the above exception, ...'
    print_exc(exc)

[…]

在2012年,PEP 415,引入异常属性的上下文抑制,通过显式的 raise EXCEPTION from None__suppress_context__ 属性)。

提议

将在 BaseException 上引入一个新属性 __suppress_context__。每当 __cause__ 被设置时,__suppress_context__ 将被设置为 True。特别是,raise exc from cause 语法将使 exc.__suppress_context__ 设置为 True。异常打印代码将检查该属性,以确定是否打印上下文和原因。__cause__ 将恢复其原始目的和值。

对于 __suppress_context__,有 print_line_and_file 异常属性的先例。

总之,raise exc from cause 将等同于:

exc.__cause__ = cause
raise exc

其中 exc.__cause__ = cause 隐式地设置了 exc.__suppress_context__

因此,在PEP 415中,PEP 3134中给出的默认异常处理程序的过程(它的工作是报告异常)变为:

def print_chain(exc):
    if exc.__cause__:
        print_chain(exc.__cause__)
        print '\nThe above exception was the direct cause...'
    elif exc.__context__ and not exc.__suppress_context__:
        print_chain(exc.__context__)
        print '\nDuring handling of the above exception, ...'
    print_exc(exc)
490

这里的区别在于,当你使用 from 时,__cause__ 属性 会被设置,并且信息会说明这个异常是 直接由某个原因引起的。如果你不使用 from,那么就不会设置 __cause__,但可能会设置 __context__ 属性,这时追踪信息会显示为 在处理过程中发生了其他事情

设置 __context__ 是在异常处理器中使用 raise 时发生的;如果你在其他地方使用 raise,则不会设置 __context__

如果设置了 __cause__,异常上还会有一个 __suppress_context__ = True 的标志;当 __suppress_context__ 被设置为 True 时,打印追踪信息时会忽略 __context__

当你在异常处理器中抛出异常时,如果 不想 显示上下文(不想看到 在处理另一个异常时发生了什么 的信息),可以使用 raise ... from None 来将 __suppress_context__ 设置为 True

换句话说,Python 在异常上设置了一个 上下文,这样你可以查看异常是在哪里被抛出的,看看是否有其他异常被替代。你还可以为异常添加一个 原因,让追踪信息更清楚地说明另一个异常(使用不同的措辞),而上下文会被忽略(但在调试时仍然可以查看)。使用 raise ... from None 可以让你抑制上下文的打印。

可以查看 raise 语句的文档

from 子句用于异常链:如果给定,第二个 表达式 必须是另一个异常类或实例,这样它就会作为 __cause__ 属性附加到抛出的异常上(这个属性是可写的)。如果抛出的异常没有被处理,这两个异常都会被打印:

>>> try:
...     print(1 / 0)
... except Exception as exc:
...     raise RuntimeError("Something bad happened") from exc
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: int division or modulo by zero

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

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened

如果在异常处理器或 finally 子句中抛出异常,也会隐式地使用类似的机制:之前的异常会作为新异常的 __context__ 属性附加上:

>>> try:
...     print(1 / 0)
... except:
...     raise RuntimeError("Something bad happened")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: int division or modulo by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened

还可以查看 内置异常文档,了解附加到异常的上下文和原因信息的详细信息。

撰写回答