Python上下文管理器with的用法

  通常我们使用with关键字,作为上下文管理器进入标志。上下文管理器是一个包装任意代码块的对象,当退出上下文管理器时,保证相关的资源能够得到正确处理。最常用的用法是打开文件时使用上下文管理器,保证文件能被关闭,无论代码块中是否发生异常都会执行关闭的操作。例如下面的代码:

with open("上下文管理器/测试.txt","r", encoding='utf-8') as f:
    contents = f.readline()
    print(contents)

  上面代码中,我们使用Python的内置函数open(...)打开一个文件,并使用f这个引用来“接收返回的结果”,在上下文管理器中,我们读取了一行内容,并打印,最后退出上下文管理器。这是我们最常见的用法,这种用法保证了,即使在代码块(2-3行)中出现了异常,也能保证文件会被关闭,而不需要我们自己去手动关闭。因此上述代码其实是和下面的try-except-finally结构是一样的作用。

try:
    f = open("上下文管理器/测试.txt","r", encoding='utf-8')
    contents = f.readline()
    print(contents)
except:
    pass
finally:
    f.close()

1、编写自己的上下文管理器

  下面我们详细的讲解with到底发生了什么事情。其实with的本质是对with紧跟的代码进行求值(本例中就是调用open()函数),该表达式执行后(open()函数执行后)一定会返回一个对象,这个对象包含有两个魔法方法: __enter__和__exit__,并且会立刻执行这个对象的__enter__方法,__enter__方法的返回值会赋给as后面的变量(此例中就会赋给变量f)。我们使用下面的例子来详细解读:

## 定义一个上下文管理器类(包含有__enter__和__exit__方法)
class ContextManager(object):
    def __init__(self) -> None:
        self.entered = False

    def __enter__(self):
        print("__enter__方法被调用")
        self.entered = True
        return self
    
    def __exit__(self, *args):
        print("__exit__方法被调用")
        self.entered = False


with ContextManager() as f:
    print("f的entered属性:", f.entered)
    print("f的类型是:", type(f))
print("退出with后, f的entered属性:", f.entered)

  在这个例子中我们创造了一个上下文管理器类ContextManager,这个类具有 __enter__和__exit__方法。在第十六行,with后面的跟的是ContextManager(),也就是类的实例化操作,当完成类的实例化后得到了一个ContextManager对象,并且会立刻执行这个对象的__enter__方法(__enter__方法只有一个self参数),__enter__方法return了对象本身并赋值给了变量f。当程序执行完18行后,就退出了上下文管理器,此时就会可以执行ContextManager对象的__exit__方法。因此最后的程序执行结果如下:

  需要注意的是,with后面的表达式执行的结果(得到的对象)并没有赋值给任何变量,而f变量接收的实际上是执行__enter__方法执行的返回值。我们看下面这个例子:

class ContextManager(object):
    def __init__(self) -> None:
        self.entered = False

    def __enter__(self):
        self.entered = True
        return "__enter__返回的字符串"
    
    def __exit__(self, *args):
        self.entered = False


with ContextManager() as f:
    print("f的类型是: ", type(f))
    print("f为: ", f)

  执行结果是:

   我们在上一个例子的基础上,将__enter__方法的返回值改成了一个字符串,所以f变量实际上接收的就是__enter__方法的返回值的字符串。而with紧跟的表达式ContextManager()执行结果(得到一个ContextManager对象)并没有被任何变量接收。一般情况下__enter__方法都是返回self本身。

2、__exit__方法

  上一节中我们讲解了上下文管理器的基本流程,以及__enter__方法,本节将详细讲解在退出上下文管理器时发生的事情。其实退出上下文管理器就是在执行__exit__方法,__exit__方法接收三个位置参数(不包括传统的self参数)分别是:1、异常类型,2、异常实例,3、错误回溯信息。__exit__方法可以选择性的处理包装代码块(即with包裹的部分)中出现的异常,以及处理其他需要关闭上下文管理器状态的事情。__exit__方法会收集with代码块中抛出的异常(也就是位置参数的含义),如果__exit__方法返回True,则会终止异常抛出到程序,如果__exit__方法返回False,则会将捕获到的异常传播出去。我们从下面的代码中来理解:

class ContextManager(object):
    def __init__(self, x) -> None:
        self.x = x

    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_instance, traceback):
        if exc_instance:
            print(f"发生了{exc_type}异常,异常信息为: {exc_instance}")
        return True

with ContextManager("传递参数") as f:
    raise TypeError("with中抛出的异常")

  运行结果是:

  我们在with代码块中抛出了一个TypeError的异常,这个异常被__exit__方法捕获,并且进行了处理(即打印了一句话),由于__exit__方法返回的是一个True因此捕获到的异常不会被继续抛出,因此程序顺利结束了。如果我们将__exit__方法中返回值改为False,那么得到的运行结果就是下图了。

  可以看出__exit__方法虽然打印了错误信息(即执行了第10行),但由于__exit__方法返回值是False,因此with中的错误依然被抛出到了控制台。

  这里需要注意的是,如果with中已经有try-except对一些异常进行了处理,那么这些异常将不会交给__exit__方法处理。因此总结来说,__exit__方法会捕获with代码块中没有被处理的异常。__exit__方法的灵活性使得在方法内部可以完成以下操作:

  • 处理特定的异常。根据捕获的异常种类不同,决定是否将异常抛出(不同异常类型返回True或者False)
  • 基于属性来处理异常。其实上个例子中的13行,在with后面我们使用ContextManager(xxx)的方式创建了一个ContextManager对象,并传递给了给对象一个属性,这个属性值会被保存在ContextManager对象中,并且在__exit__方法中可以使用这个属性。因此我们可以根据不同的属性值,在退出with时进行不同的操作

3、总结

  上下文管理器提供了确保资源被正确处理的优秀方式,如果需要重复的使用同一个try-except-finally处理异常代码,那我们可以将其封装成上下文管理器。在__exit__方法中可以执行关闭资源的操作,这使得with代码块中出现异常时也能正确的关闭资源,通常这是非常重要的。

 

posted @ 2023-03-18 17:32  Circle_Wang  阅读(55)  评论(0编辑  收藏  举报