一旦进入 finally 块,如何确定是否引发了异常?

How to determine if an exception was raised once you're in the finally block?

一旦进入 finally 子句,是否可以判断是否存在异常?类似于:

try:
    funky code
finally:
    if ???:
        print('the funky code raised')

我想把这样的东西做得更干:

try:
    funky code
except HandleThis:
    # handle it
    raised = True
except DontHandleThis:
    raised = True
    raise
else:
    raised = False
finally:
    logger.info('funky code raised %s', raised)

我不喜欢它需要捕获一个您不打算处理的异常,只是为了设置一个标志。


由于一些 在 MCVE 中要求较少 "M",这里有一些关于用例的更多背景。实际问题是关于日志记录级别的升级。

因此,代码在日志捕获上下文中运行(它设置自定义处理程序来拦截日志记录)并且一些调试信息被追溯地重新记录:

try:
    with LogCapture() as log:
        funky_code()  # <-- third party badness
finally:
    # log events are buffered in memory. if there was an exception,
    # emit everything that was captured at a WARNING level
    for record in log.captured:
        if <there was an exception>:
            log_fn = mylogger.warning
        else:
            log_fn = getattr(mylogger, record.levelname.lower())
        log_fn(record.msg, record.args)

如果是我,我会 re-ordering 你的代码。

raised = False
try:
    # funky code
except HandleThis:
    # handle it
    raised = True
except Exception as ex:
    # Don't Handle This 
    raise ex
finally:
    if raised:
        logger.info('funky code was raised')

我已将引发的布尔值赋值放在 try 语句之外以确保范围,并使最终的 except 语句成为您不想处理的异常的通用异常处理程序。

此样式决定您的代码是否失败。另一种方法可能会确定您的代码何时成功。

success = False
try:
    # funky code
    success = True
except HandleThis:
    # handle it
    pass
except Exception as ex:
    # Don't Handle This 
    raise ex
finally:
    if success:
        logger.info('funky code was successful')
    else:
        logger.info('funky code was raised')

您可以轻松地将捕获的异常分配给一个变量并在 finally 块中使用它,例如:

>>> x = 1
>>> error = None
>>> try:
...     x.foo()
... except Exception as e:
...     error = e
... finally:
...     if error is not None:
...             print(error)
...
'int' object has no attribute 'foo'
raised = True
try:
    funky code
    raised = False
except HandleThis:
    # handle it
finally:
    logger.info('funky code raised %s', raised)

鉴于添加到有关选择日志级别的问题中的额外背景信息,这似乎很容易适应预期 use-case:

mylog = WARNING
try:
    funky code
    mylog = DEBUG
except HandleThis:
    # handle it
finally:
    mylog(...)

使用上下文管理器

您可以使用自定义上下文管理器,例如:

class DidWeRaise:
    __slots__ = ('exception_happened', )  # instances will take less memory

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # If no exception happened the `exc_type` is None
        self.exception_happened = exc_type is not None

然后在 try:

中使用
try:
    with DidWeRaise() as error_state:
        # funky code
finally:
    if error_state.exception_happened:
        print('the funky code raised')

它仍然是一个额外的变量,但如果您想在多个地方使用它,它可能更容易重用。而且您不需要自己切换它。

使用变量

如果您不想要上下文管理器,我会反转触发器的逻辑并切换它 以防 异常已经发生了。这样你就不需要 except 案例来处理你不想处理的异常。最合适的位置是在 try 没有抛出异常的情况下输入的 else 子句:

exception_happened = True
try:
    # funky code
except HandleThis:
    # handle this kind of exception
else:
    exception_happened = False
finally:
    if exception_happened:
        print('the funky code raised')

正如已经指出的那样,您可以用所需的日志记录函数替换它(在本例中)而不是 "toggle" 变量:

mylog = mylogger.WARNING
try:
    with LogCapture() as log:
        funky_code()
except HandleThis:
    # handle this kind of exception
else:
    # In case absolutely no exception was thrown in the try we can log on debug level
    mylog = mylogger.DEBUG
finally:
    for record in log.captured:
        mylog(record.msg, record.args)

当然,如果将它放在 try 的末尾也可以(正如此处建议的其他答案),但我更喜欢 else 子句,因为它具有更多含义("that code is meant to be executed only if there was no exception in the try block") 并且在长 运行 中可能更容易维护。虽然它仍然比上下文管理器更需要维护,因为变量是在不同的地方设置和切换的。

使用 sys.exc_info(仅适用于未处理的异常)

我想提到的最后一种方法可能对您没有用,但可能对未来的读者有用,他们只想知道是否存在 未处理 异常(异常是 not 陷入任何 except 块或已在 except 块内引发)。在这种情况下,您可以使用 sys.exc_info:

import sys

try:
    # funky code
except HandleThis:
    pass
finally:
    if sys.exc_info()[0] is not None:
        # only entered if there's an *unhandled* exception, e.g. NOT a HandleThis exception
        print('funky code raised')

好吧,听起来您实际上只是想修改现有的上下文管理器,或者使用类似的方法:logbook 实际上有一个叫做 FingersCrossedHandler 的东西,它可以做的正是你要。但是你可以自己做,比如:

@contextmanager
def LogCapture():
    # your existing buffer code here
    level = logging.WARN
    try:
        yield
    except UselessException:
        level = logging.DEBUG
        raise  # Or don't, if you just want it to go away
    finally:
        # emit logs here

原始回复

你考虑的有点偏了。

打算处理异常 - 您正在通过设置标志来处理它。也许您不关心其他任何事情(这似乎是个坏主意),但如果您关心在引发 an 异常时做某事,那么您想要明确说明。

您正在设置一个变量,但您希望异常继续,这意味着您真正想要的是引发您自己的特定异常,从异常提出:

class MyPkgException(Exception):  pass
class MyError(PyPkgException): pass  # If there's another exception type, you can also inherit from that

def do_the_badness():
    try:
        raise FileNotFoundError('Or some other code that raises an error')
    except FileNotFoundError as e:
        raise MyError('File was not found, doh!') from e
    finally:
        do_some_cleanup()

try:
    do_the_badness()
except MyError as e:
    print('The error? Yeah, it happened')

这解决了:

  • 明确处理您要处理的异常
  • 使堆栈跟踪和原始异常可用
  • 允许将要在其他地方处理原始异常的代码来处理抛出的异常
  • 允许一些 top-level 异常处理代码只捕获 MyPkgException 以捕获所有异常,以便它可以记录一些东西并以良好的状态退出而不是丑陋的堆栈跟踪

If exception happened --> Put this logic in the exception block(s).
If exception did not happen --> Put this logic in the try block after the point in code where the exception can occur.

Finally blocks should be reserved for "cleanup actions," according to the Python language reference. 当 finally 被指定时,解释器在 except 情况下进行如下:异常被保存,然后finally 块 首先 执行,然后最后引发异常。