一般来说,适合使用上下文管理器的情况都涉及确保某种资源以一种期望的方式被初始化和反初始化,或尽力避免重复(尤其是异常处理)。
资源清理
打开和关闭资源是编写上下文管理器的重要因素之一,确保发生异常时正确关闭资源往往很重要,这样可以避免最终随着时间的推移而产生很多的僵尸进程。
上下文管理器的优势就在于,通过在__enter__方法中打开资源并返回它,可以保证__enter__方法能执行,并且在异常出现之前关闭这个资源(这次因为被包装的代码块的内部异常会被__exit__方法捕获)。
考虑如下这个打开PostgreSQL数据库连接的上下文管理器:
# coding=utf8
import psycopg2
class DBConnection(object):
def __init__(self, dbname=None, user=None, passwd=None, host="localhost"):
self.host = host
self.dbname = dbname
self.user = user
self.passwd = passwd
def __enter__(self):
self.connection = psycopg2.connect(
dbname=self.dbname, host=self.host,
user=self.user, password=self.passwd)
return self.connection.cursor()
def __exit__(self, ex_type, ex_instance, traceback): # 离开时关闭数据库连接
self.connection.close()
if __name__ == "__main__":
with DBConnection(user='root', dbname='fool', passwd='12345') as db:
db.execute('SELECT 1+1')
print(db.fetchall())
#这里会打印[(2,)]
而只要上下文管理器存在,分配给db的数据库指针总会被关闭,在离开上下文管理器的作用范围后,db再次查询便无法成功:
if __name__ == "__main__":
with DBConnection(user='root', dbname='fool', passwd='12345') as db:
db.execute('SELECT 1+1')
print(db.fetchall())
# [(2,)]
db.execute('SELECT 1+1')
# 这里离开了上下文管理器的范围,db对应的数据库连接已经被关闭,
# 所以抛异常:cursor already closed
大多数与数据库相关的框架都会处理数据库连接的打开和关闭操作,但原则依旧是:如果打开一个资源,一定要确保正确地关闭它,此时上下文管理器就是一个十分优秀的工具。
避免重复(异常处理)
当提到避免重复时,异常处理是最为常见的,上下文管理器能够传播和终止异常,这使得最适合将它与except子句放在同一个地方定义。
1、传播异常
如果__exit__方法只是向流程链上传播异常,只需要通过返回False实现,根本不需要与异常实例交互。考虑如下的上下文管理器:
class BubbleExceptions(object):
def __enter__(self):
return self
def __exit__(self, ex_type, ex_instance, traceback):
if ex_instance:
print('Bubbling up exception: %s.' % ex_instance)
return False # 这里返回False,会继续传播异常
with BubbleExceptions():
1+1
with BubbleExceptions():
5/0
1+1的部分并不会发生什么,关键在于5/0,会发生如下现象:
Bubbling up exception: division by zero.
Traceback (most recent call last):
File "d:\pyproject\ReviewPython\高级特性\上下文管理器.py", line 36, in <module>
5/0
ZeroDivisionError: division by zero
Bubbling up exception: division by zero.的打印说明上下文管理器对应的__exit__方法起了作用,虽然5/0的异常首先发送至__exit__方法,但由于其最后返回False,所以异常被重新抛出。
2、终止异常
对比之前__exit__方法返回False,异常会被重新抛出,而当返回True时,就起到了终止异常的效果。
class BubbleExceptions(object):
def __enter__(self):
return self
def __exit__(self, ex_type, ex_instance, traceback):
if ex_instance:
print('Bubbling up exception: %s.' % ex_instance)
return True # 这里返回True,会终止异常
with BubbleExceptions():
1+1
with BubbleExceptions():
5/0
在这里,会有如下打印:
Bubbling up exception: division by zero.
没有出现刚刚的异常回溯信息,这正是因为__exit__方法返回了True,异常没有被继续传递,即所谓的终止异常的效果。这个等价于如下常见的异常处理结构:
try:
"""do something"""
except:
pass
可以说,这种处理啰嗦且不明智。
需要注意的是,并不是所有上下文块内的异常都会被发送至__exit__方法,例如已经在上下文块内被try-except捕获处理的异常,就不会被发送。
3、处理特定类型的异常(不处理它的子类)
根据前面的介绍,__exit__内部可以进行相当细致灵活的异常处理,在传统的try-except代码块中,如果只是想捕获一个特定的异常类型,而不希望捕获它的子类异常,是无法做到的,但在上下文管理器中,就能处理这种极端情景。如下:
class ValueErrorSubclass(ValueError):
pass
class HandleValueError(object):
def __enter__(self):
return self
def __exit__(self, ex_type, ex_instance, traceback):
if not ex_instance:
return True
if ex_type == ValueError:
print("处理ValueError:%s" % ex_instance)
return True
return False
with HandleValueError():
raise ValueErrorSubclass("subclass")
这里会有如下打印:
Traceback (most recent call last):
File "d:\pyproject\ReviewPython\高级特性\上下文管理器.py", line 63, in <module>
raise ValueErrorSubclass("subclass")
__main__.ValueErrorSubclass: subclass
这是因为上下文块中抛出的是ValueError的子类,所有异常会被继续传递,而不被处理。如果抛出的是ValueError,则结果会大不相同:
with HandleValueError():
raise ValueError("not subclass")
打印如下:
处理ValueError:not subclass
结果显而易见,ValueError类型的异常被处理。