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代码块中出现异常时也能正确的关闭资源,通常这是非常重要的。