Python中的"raise from"用法

426

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

23
你读过PEP-3134吗? - jonrsharpe
5
现在使用raise IndexError from None - Martijn Pieters
25
嘿。raise IndexError from False引发的是TypeError而不是IndexError。让我开心了一整天。 - Mad Physicist
不确定是否适合提及,但对于任何使用Spyder的人: 整个结构在那里都无法工作。这已经是一个问题超过3年了(https://github.com/spyder-ide/spyder/issues/2943),但他们似乎认为没有必要使用链式异常。 - Emil Bode
4
一个指向 pep-3134 的有效链接。 - SuperShoot
3个回答

481
区别在于,当你使用from时,将设置__cause__属性,并且消息会说明异常是由直接导致的。如果省略from,则不会设置__cause__,但__context__属性可能也会被设置,并且回溯会显示上下文为“在处理其他事情时发生”。
如果在异常处理程序中使用了raise,则会设置__context__;如果在其他任何地方使用raise,则也不会设置__context__
如果设置了__cause__,则还会在异常上设置__suppress_context__ = True标志;当__suppress_context__设置为True时,在打印回溯时将忽略__context__
当在异常处理程序中抛出异常时,如果你不想显示上下文(不想显示“在处理另一个异常时发生了”消息),则使用raise ... from None来将__suppress_context__设置为True
换句话说,Python在异常上设置了一个上下文,以便你可以内省异常的发生位置,让你看到是否有其他异常被替换了。你还可以向异常添加一个原因,使回溯明确指出其他异常(使用不同的措辞),并忽略上下文(但仍可在调试时内省)。使用raise ... from None可以让你禁止打印上下文。
参见raise语句文档

The from clause is used for exception chaining: if given, the second expression must be another exception class or instance, which will then be attached to the raised exception as the __cause__ attribute (which is writable). If the raised exception is not handled, both exceptions will be printed:

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

A similar mechanism works implicitly if an exception is raised inside an exception handler or a finally clause: the previous exception is then attached as the new exception’s __context__ attribute:

>>> 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
此外,请参阅内置异常文档,了解与异常相关的上下文和原因信息的详细信息。

35
在使用except捕获异常时,是否有必要明确使用from__cause__来链接异常,而不是隐式使用__context__?是否存在一些情况需要将不同于except捕获的异常附加到该语句上? - darkfeline
36
假设您的数据库API支持从各种来源(包括Web和磁盘)打开数据库。如果打开数据库失败,则API将始终引发“DatabaseError”。但是,如果失败是由于文件无法打开导致的“IOError”或URL无法工作导致的“HTTPError”,那么这就是您希望明确包含的上下文,以便使用API的开发人员可以调试原因。在这种情况下,您可以使用“raise DatabaseError from original_exception”。 - Martijn Pieters
8
如果那个开发人员将数据库API的使用包装在他们自己的API中,并希望将IOErrorHTTPError传递给他们的消费者,那么他们必须使用raise NewException from databaseexception.__cause__,现在使用一个不同于他们刚刚捕获的DatabaseException的异常。 - Martijn Pieters
3
没有,异常链是纯粹的 Python 3 特性。 - Martijn Pieters
11
你的意思是在处理异常foo时,想要引发一个新的异常bar?那么你可以使用 raise bar from foo,Python会指出foo直接导致了bar。如果 使用from foo,Python仍然会打印两者,但是表明*在处理foo期间引发了bar*,这是一条不同的消息,旨在标记错误处理中可能存在的错误。 - Martijn Pieters
显示剩余14条评论

19

在2005年,PEP 3134, 异常链和嵌套回溯引入了异常链:

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

Motivation

During the handling of one exception (exception A), it is possible that another exception (exception B) may occur. In today’s Python (version 2.4), if this happens, exception B is propagated outward and exception A is lost. In order to debug the problem, it is useful to know about both exceptions. The __context__ attribute retains this information automatically.

Sometimes it can be useful for an exception handler to intentionally re-raise an exception, either to provide extra information or to translate an exception to another type. The __cause__ attribute provides an explicit way to record the direct cause of an exception.

[…]

Implicit Exception Chaining

Here is an example to illustrate the __context__ attribute:

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()

Calling compute(0, 0) causes a ZeroDivisionError. The compute() function catches this exception and calls log(exc), but the log() function also raises an exception when it tries to write to a file that wasn’t opened for writing.

In today’s Python, the caller of compute() gets thrown an IOError. The ZeroDivisionError is lost. With the proposed change, the instance of IOError has an additional __context__ attribute that retains the ZeroDivisionError.

[…]

Explicit Exception Chaining

The __cause__ attribute on exception objects is always initialized to None. It is set by a new form of the raise statement:

raise EXCEPTION from CAUSE

which is equivalent to:

exc = EXCEPTION
exc.__cause__ = CAUSE
raise exc

In the following example, a database provides implementations for a few different kinds of storage, with file storage as one kind. The database designer wants errors to propagate as DatabaseError objects so that the client doesn’t have to be aware of the storage-specific details, but doesn’t want to lose the underlying error information.

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

If the call to open() raises an exception, the problem will be reported as a DatabaseError, with a __cause__ attribute that reveals the IOError as the original cause.

Enhanced Reporting

The default exception handler will be modified to report chained exceptions. The chain of exceptions is traversed by following the __cause__ and __context__ attributes, with __cause__ taking priority. In keeping with the chronological order of tracebacks, the most recently raised exception is displayed last; that is, the display begins with the description of the innermost exception and backs up the chain to the outermost exception. The tracebacks are formatted as usual, with one of the lines:

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

or

During handling of the above exception, another exception occurred:

between tracebacks, depending whether they are linked by __cause__ or __context__ respectively. Here is a sketch of the procedure:

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, Implement Context Suppression with Exception Attributes引入了异常上下文抑制的显式方法raise EXCEPTION from None__suppress_context__属性)。

Proposal

A new attribute on BaseException, __suppress_context__, will be introduced. Whenever __cause__ is set, __suppress_context__ will be set to True. In particular, raise exc from cause syntax will set exc.__suppress_context__ to True. Exception printing code will check for that attribute to determine whether context and cause will be printed. __cause__ will return to its original purpose and values.

There is precedence for __suppress_context__ with the print_line_and_file exception attribute.

To summarize, raise exc from cause will be equivalent to:

exc.__cause__ = cause
raise exc

where exc.__cause__ = cause implicitly sets 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)

5
最简短的回答PEP-3134已经说明了一切。 raise Exception from e将新异常的__cause__字段设置为原始异常。

来自同一PEP的更长的回答:

  • except:块内部隐式地将__context__字段设置为原始错误,除非使用__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

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