1. 引言
在Python中,我们难免会操作文件、连接断开数据库,这些都是很常见的操作,在文件操作的时候,我们必须保证打开文件后调用close()
,连接数据库后也需要close()
操作,不然就容易造成资源泄露,轻则系统处理缓慢,重则系统崩溃。
老规矩,我们从一个例子入手:
for x in range(10000000):
f =open('test.txt','w')
f.write('hello')
很明显,这样的疯狂的行为会导致错误,因为我们一共打开了1000万次文件而没有关闭,占用了太多资源,造成了系统崩溃。
为了解决这个问题,Python中引用了上下文管理器(context manager),上下文管理器能够帮助你自动分配并且释放资源,其中最经典的就是with
语句,对于上面的例子,我们一般是使用上下文管理器进行文件操作的:
for x in range(10000000):
with open('test.txt','w') as f:
f.write('hello')
这样使用的话,当每次打开文件之后,Python就会帮我们自动关闭文件,相应的资源也可以得到释放,当然我们也可以改写为:
f = open('test.txt','w')
try:
f.write('hello')
finally:
f.close()
其中,我们还可以通过try/except/finally
捕获异常,不过相对于with
来说,这样的代码就显得很冗余。
2. 上下文管理器的实现
2.1 基于类的上下文管理器
我们通过一个例子,搞清楚上下文管理器的原理,搞清楚它的内部实现。在下面的例子中,我们手动实现一个上下文管理器FileSystem,手动定义它的打开关闭操作。
class FileSystem:
def __init__(self, name, mode):
print('调用init方法')
self.name = name
self.mode = mode
self.file = None
def __enter__(self): # 定义enter方法
print('调用enter方法')
self.file = open(self.name, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb): # 定义exit方法
print('调用exit方法')
if self.file:
self.file.close()
with FileSystem('text.txt', 'w') as f:
print('准备写入文件')
f.write('hello world')
# 输出
调用init方法
调用enter方法
准备写入文件
调用exit方法
从我们实现的上下文管理器可以看出,调用的步骤为:
- 首先调用
__init__
方法,初始化对象FileSystem
,此时传入的文件名为test.txt
,模式为'w'
- 方法
__enter__
被调用,文件test.txt
以写入模式打开,并且返回FileSystem
对象赋予变量f- 字符串
'hello world'
被写入文件- 方法
__exit__
被调用,关闭之前打开的文件流
其中的__exit__
方法中的参数exc_type, exc_val, exc_tb
,分别表示exception_type、exception_value、exception_traceback。当我们使用with
语句时候,如果发生异常,那么异常信息就会包括在这三个变量中,传入方法__exit__()
中。
下面我们在__exit__()
中添加捕获异常。
class Error:
def __init__(self):
print('调用__init__')
def __enter__(self):
print('调用__enter__ ')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('调用__exit__')
if exc_type:
print(f'exception_type:{exc_type}')
print(f'exception_value:{exc_val}')
print(f'exception_traceback:{exc_tb}')
return True
with Error() as obj:
raise Exception('exception raised').with_traceback(None)
# 输出
调用__init__
调用__enter__
调用__exit__
exception_type:<class 'Exception'>
exception_value:exception raised
exception_traceback:<traceback object at 0x7fe0a72c5a00>
2.2 简单的基于上下文管理器的数据库连接
class DBConnectionManager:
def __init__(self, hostname, port):
self.hostname = hostname
self.port = port
self.connection = None
def __enter__(self):
self.connection = DBClient(self.hostname, self.port)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.close()
with DBConnectionManager('127.0.0.1', '8080') as dbclient:
pass
2.3 基于生成器的上下文管理器
基于类的上下文管理器是我们最常见也是最常用的形式,不过Python中的上下文管理器除了可以基于类实现,还可以基于生成器实现。
比如,我们可以通过装饰器contextlib.contextmanager
,来定义自己所需的基于生成器的上下文管理器,也可以支持with语句,我们通过例子来实现:
from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
try:
f = open(name, mode)
yield f
finally:
f.close()
with file_manager('test.txt', 'w') as f:
f.write('hello world')
在这段代码中,file_manager()
是一个生成器,当我们执行with语句的时候,便会打开执行文件,并返回文件对象f,当with语句执行完成后,finnally block 中的关闭文件操作便会执行。
当我们使用基于生成器的上下文管理器时,我们就不用再定义
__enter__()
方法和__exit__()
方法了,但是必须记得加上@contextmanager
基于类的上下文管理器和基于生成器的上下文管理器在功能上是一致的,但是:
- 基于类的上下文管理器更灵活,适用于大型系统的开发;
- 基于生成器的上下文管理器更加方便、简洁,适用于中小型程序开发;
3. 总结
上下文管理器一共分为两种:①基于类的上下文管理器②基于生成器的上下文管理器
具体的使用哪种上下文管理器,要结合使用场景。上下文管理器通常和with语句一起使用,大大提高了程序的简洁程度,上下文管理器一般都包括enter、exit
这两个部分,一旦有异常抛出,异常的类型、异常的值、异常的回溯对象等信息都会通过参数传入__exit__()
函数中,我们可以自定义相关操作对异常进行处理。
Q.E.D.