【问题标题】:Return in finally block in python context manager在 python 上下文管理器中的 finally 块中返回
【发布时间】:2014-08-24 11:52:33
【问题描述】:

我最近在 Python 的 with 语句中遇到了一个奇怪的行为。我有一个代码,它使用 Python 的上下文管理器来回滚 __exit__ 方法中的配置更改。经理在 __exit__ 的 finally 块中有一个 return False 值。我已经在下面的代码中隔离了这种情况 - 唯一的区别是 return 语句的缩进:

class Manager1(object):

    def release(self):
        pass # Implementation not important

    def rollback(self):
        # Rollback fails throwing an exception:
        raise Exception("A failure")

    def __enter__(self):
        print "ENTER1"

    def __exit__(self, exc_type, exc_val, exc_tb):
        print "EXIT1"
        try:
            self.rollback()
        finally:
            self.release()
            return False          # The only difference here!


class Manager2(object):

    def release(self):
        pass # Implementation not important

    def rollback(self):
        # Rollback fails throwing an exception:
        raise Exception("A failure")

    def __enter__(self):
        print "ENTER2"

    def __exit__(self, exc_type, exc_val, exc_tb):
        print "EXIT2"
        try:
            self.rollback()
        finally:
            self.release()
        return False      # The only difference here!

在上面的代码中,回滚失败并出现异常。我的问题是,为什么Manager1 的行为与Manager2 不同。异常不会在Manager1 中的 with 语句之外引发,以及为什么它会在Manager2 中退出时引发。

with Manager1() as m:          
    pass                  # The Exception is NOT thrown on exit here


with Manager2() as m:
    pass                  # The Exception IS thrown on exit here

根据documentation of __exit__

如果提供了异常,并且该方法希望抑制 异常(即,防止它被传播),它应该返回一个 真正的价值。否则,异常将正常处理 退出此方法。

在我看来,在这两种情况下 exit 都没有返回 True,因此在这两种情况下都不应该抑制异常。但是在 Manager1 中它是。谁能解释一下?

我使用 Python 2.7.6。

【问题讨论】:

标签: python with-statement contextmanager


【解决方案1】:

如果激活了finally 子句,则意味着try 块已成功完成,或者它引发了已处理的错误,或者try 块执行了return

在 Manager1 中,return 语句作为finally 子句的一部分的执行使其正常终止,返回False。在您的 Manager2 类中,finally 子句仍然执行,但如果它是由于引发异常而执行的,则它不会阻止该异常沿调用链向上传播,直到被捕获(或直到它终止您的程序并带有回溯)。

Manager2.__exit__() 只会在没有引发异常的情况下返回 False。

【讨论】:

  • 我相信举个例子会更清楚。像def test(): try: raise Exception(); finally: return True 这样的东西并表明调用test 不会引发任何异常。
  • In docs here: "如果 finally 子句引发另一个异常或执行 return 或 break 语句,则丢弃保存的异常"
  • @Bakuiriu 谢谢,你是对的,我很乐意让你编辑答案。我现在有点紧张。
  • 或者try 块处于循环中,并且breakcontinuetry 套件中发出。例如。退出try 的任何内容都会触发finally。 :-)
【解决方案2】:

我认为理解这一点的一个好方法是查看一个独立于所有上下文管理器内容的单独示例:

>>> def test ():
        try:
            print('Before raise')
            raise Exception()
            print('After raise')
        finally:
            print('In finally')
        print('Outside of try/finally')

>>> test()
Before raise
In finally
Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    test()
  File "<pyshell#6>", line 4, in test
    raise Exception()
Exception

因此您可以看到,当try 块内引发异常时,会执行该异常之前 的任何代码,并且会执行finally 块内的任何代码。除此之外,其他所有内容都被跳过。那是因为被抛出的异常结束了函数调用。但由于异常是在 try 块内引发的,因此相应的 finally 块有最后的运行机会。

现在,如果您注释掉函数中的raise 行,您将看到所有代码都已执行,因为函数不会提前结束。

【讨论】: