关于python:如果确定在finally块中是否引发了异常?

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

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

1
2
3
4
5
try:
    funky code
finally:
    if ???:
        print('the funky code raised')

我想要做更像这样的事情干:

1
2
3
4
5
6
7
8
9
10
11
12
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"较少,因此这里有一些关于用例的更多背景知识。实际问题是关于日志记录级别的升级。

  • 时髦的代码是第三方,无法更改。
  • 故障异常和堆栈跟踪不包含任何有用的诊断信息,因此在except块中使用logger.exception在此处没有帮助。
  • 如果引发了时髦的代码,那么我需要查看的一些信息已经被记录在DEBUG级别。我们没有也无法处理错误,但是想要升级DEBUG日志记录,因为所需的信息就在那里。
  • 大多数时候,时髦的代码不会引发。我不想升级一般情况下的日志记录级别,因为它太冗长了。

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

1
2
3
4
5
6
7
try:
    with LogCapture() as log:
        funky_code()  # <-- third party badness
finally:
    mylog = mylogger.WARNING if <there was exception> else mylogger.DEBUG
    for record in log.captured:
        mylog(record.msg, record.args)


使用上下文管理器

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

1
2
3
4
5
6
7
8
9
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中使用它:

1
2
3
4
5
6
try:
    with DidWeRaise() as error_state:
        # funky code
finally:
    if error_state.exception_happened:
        print('the funky code raised')

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

使用变量

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

1
2
3
4
5
6
7
8
9
10
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')

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

1
2
3
4
5
6
7
8
9
10
11
12
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子句,因为它有更多的含义("那个代码只有在那里才能执行在try块中也不例外,并且从长远来看可能更容易维护。虽然它比上下文管理器更需要维护,因为变量在不同的地方设置和切换。

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

我想提到的最后一种方法可能对你没用,但对于那些只想知道是否存在未处理的异常(未在任何except块中捕获或已在块)。在这种情况下,您可以使用sys.exc_info

1
2
3
4
5
6
7
8
9
10
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')


1
2
3
4
5
6
7
8
raised = True
try:
    funky code
    raised = False
except HandleThis:
    # handle it
finally:
    logger.info('funky code raised %s', raised)

鉴于在选择日志级别的问题中添加了额外的背景信息,这似乎很容易适应预期的用例:

1
2
3
4
5
6
7
8
mylog = WARNING
try:
    funky code
    mylog = DEBUG
except HandleThis:
    # handle it
finally:
    mylog(...)


好吧,所以听起来你实际上只想修改现有的上下文管理器,或者使用类似的方法:logbook实际上有一个叫做FingersCrossedHandler的东西,可以完全按照你的意愿行事。但你可以自己做,比如:

1
2
3
4
5
6
7
8
9
10
11
@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

原始回应

你在想这个有点横向。

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

您正在设置变量,但希望异常继续的事实意味着您真正想要的是从引发的异常中引发您自己的特定异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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')

这解决了:

  • 明确处理您要处理的异常
  • 使堆栈跟踪和原始异常可用
  • 允许您的代码在其他地方处理原始异常以处理抛出的异常
  • 允许一些顶级异常处理代码只捕获MyPkgException来捕获所有异常,这样它就可以记录一些东西并以一个漂亮的状态而不是一个丑陋的堆栈跟踪退出


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

1
2
3
4
5
6
7
8
9
10
11
>>> 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'


如果是我,我会对您的代码进行一些重新排序。

1
2
3
4
5
6
7
8
9
10
11
12
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语句成为您不想处理的异常的一般异常处理程序。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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')