在Python上下文管理器中在finally块中返回

2 投票
2 回答
3645 浏览
提问于 2025-04-18 11:59

我最近在使用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

根据关于__exit__的文档

如果有异常被提供,并且这个方法希望抑制这个异常(也就是说,防止它被传播),它应该返回一个真值。否则,异常将在退出这个方法时正常处理。

在我看来,在这两种情况下,exit都没有返回True,因此异常在这两种情况下都不应该被抑制。然而在Manager1中却抑制了。有人能解释一下吗?

我使用的是Python 2.7.6。

2 个回答

1

我觉得理解这个问题的一个好方法是看一个和上下文管理器无关的例子:

>>> 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 行注释掉,你会发现所有的代码都会被执行,因为函数不会提前结束。

5

如果 finally 这个部分被执行了,说明要么 try 里面的代码顺利完成了,要么是出现了错误但已经处理过了,或者 try 里面执行了 return

在 Manager1 里,finally 里面的 return 语句让它正常结束,并返回 False。而在你的 Manager2 类中,finally 还是会执行,但如果是因为出现了异常而执行的,它不会阻止这个异常继续向上冒泡,直到被捕获(或者导致你的程序崩溃并显示错误追踪信息)。

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

撰写回答