Python基础教程7--上下文管理器
作用和使用场景
上下文管理器主要作用是在执行某项操作任务之前做一些预处理工作,执行后再做一些清理工作,可以让我们以更加优雅和安全的方式使用资源。
在需要管理一些资源比如文件、网络连接和锁的编程环境中,使用上下文管理器是很普遍的。 这些资源的一个主要特征是,它们必须被手动的关闭或释放来确保程序的正确运行。例如,如果你请求了一个锁,那么你必须确保使用之后释放了它,否则就可能产生死锁。
而上下文管理器的运行机制很容易就可以确保资源的正确使用,通过实现__enter__()和__exit__()方法,并使用with语句可以很容易实现这些操作逻辑,从而让我们无需担心资源的使用。
更加优雅
我们现在接到一个需求,要写一个操作文件的通用方法,首先我们分析下操作文件的流程
- 打开文件 -- 操作文件之前,必须要打开文件
- 文件操作 -- 进行文件的读写操作,这是变化部分,不同的任务对于文件的读写操作不一样
- 关闭文件 -- 最后一定要关闭文件,否则文件会被锁住,不能被其他应用访问
基本上所有的文件操作都是这个流程。
对于这个需求,我们首先会想到封装一个函数,操作部分作为其中的一个参数(回调函数),在调用时将具体的操作函数传递到封装的函数中,代码如下
def open_file(file_name, callback, mode='a'):
f = open(file_name, mode) # 打开文件
try:
callback(f) # 操作文件
finally:
f.close() # 关闭文件
open_file('test.txt', lambda f: f.write('Hello, World!!!')) #测试代码
到目前为止,open_file函数完全满足我们对于文件操作通用方法的需求,不过让我们思考一下,有没有改进的空间,毕竟回调函数看着不那么"优雅"。。。
这时我们想到了上下文管理器的定义和使用场景,它的特点非常符合我们的功能需求,那我们尝试用上下文管理器改写下刚才的函数,代码如下
from contextlib import contextmanager
@contextmanager
def ctx_open_file(file_name, mode='a'):
f = open(file_name, mode) # 打开文件
try:
yield f # 将_file回传到with语句中
finally:
f.close() # 关闭
with ctx_open_file('test.txt', 'a') as f: #测试代码
f.write('Hello, World!!!') # 操作文件
可以看到关于函数的封装部分差别不是很大,但是在使用封装函数时差别比较大,通过上下文管理器,代码要"优雅"的多。
这也是使用上下文环境的主要原因之一,虽然和功能无关,但代码更加优雅。
封装上下文管理器
接下来我们具体看一下上下文管理器的封装和使用,上下文管理器要和with语句一起使用,语法如下
with EXPR as VAR:
BLOCK
with可以用到很多地方,上下文管理器只是其中一个使用场景
可以看到通过with语句使用上下文管理器非常简单,使用过程如下
- 执行EXPR语句,获取上下文管理器
- 调用上下文管理器中的__enter__方法,该方法执行一些预处理工作
- 将__enter__方法的返回值赋值给变量VAR,这里的as VAR可以省略
- 执行代码块BLOCK,这里的VAR当做普通变量使用
- 最后调用上下文管理器中的的__exit__方法,做相关清理工作
为了让一个对象兼容 with 语句,需要实现__enter__()和__exit__()方法,我们看一个示例
#上下文管理器实现
class OpenFile(object):
def __init__(self, filename, mode):
print("--init--")
self.filename = filename
self.mode = mode
def __enter__(self):
print("--enter--")
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_ty, exc_val, exc_tb):
print("--exit--")
self.file.close()
#return True # 如果返回True,不论with中发生什么,with后面的程序会继续正常执行
#使用上下文管理器
open_file = OpenFile('test.txt', 'a') # 调用OpenFile的__init__构造方法,获取一个上下文管理器,此时并未调用__enter__
with open_file as f: # 调用__enter__方法,打开文件,并将文件对象作为返回值赋值给f
print('--write--')
f.write('Hello, World!!!') # 使用f操作文件
# 调用__exit__方法,关闭文件
print("--next--") # 继续执行
#打印结果如下:
#--init--
#--enter--
#--write--
#--exit--
#--next--
不管with代码块中发生什么,上面的控制流都会执行完,就算代码块中发生了异常也是一样的。 事实上,__exit__() 方法的三个参数包含了异常类型、异常值和追溯信息(如果有的话)。__exit__() 方法能自己决定怎样利用这个异常信息,或者忽略它并返回一个None值。 如果 __exit__() 返回 True,那么异常会被忽略,就好像什么都没发生一样, with语句后面的程序会继续正常执行。这一机制保证了资源的正确使用,这也是我们使用上下文管理器的另外一个主要原因。
请注意,这里只是为了演示功能,其实open本上就是一个上下文管理器,可以直接跟with一起使用。
简单实现方法
通过封装一个带有__enter__和__exit__方法的类是实现上下文管理器的方式之一,不过代码稍显复杂,要写很多模板代码,幸运的是Python提供了一个更加简单的方法,那就是使用contexlib模块中的@contextmanager装饰器。
我们使用contextmanager装饰器改写一下OpenFile示例,代码如下
from contextlib import contextmanager
@contextmanager
def openFile(filename, mode): # 类变为了函数,代码简化了很多
f = open(filename, mode)
try:
yield f # yield语句后的f会传递到with语句中,作为VAR变量使用
finally:
f.close()
with openFile('test.txt', 'a') as f:
f.write('Hello, World!!!')
yield之前的代码会在上下文管理器中作为 __enter__() 方法执行, 所有在yield之后的代码会作为 __exit__() 方法执行。 如果出现了异常,异常会在yield语句那里抛出。