正好前几天有小伙伴在「测试开发群」里问Python 上下文管理器有哪些使用场景。我感觉多数人应该经常用,但是换了个问法后就有点陌生了,所以我花点时间给大家整理下 Python 上下文管理器的知识点。
在 Python 中,读写文件我们经常用下面这种写法:
with open("test_file.txt", "w+") as test_file:
print(test_file)
test_file.write("hello world")
这里其实就用到了 with 上下文管理器。它的工作原理是什么呢?
我们从代码字面意思可以猜到 with open……语句返回的是一个操作文件的句柄对象(打印出来的值是:)。
然后我们直接使用这个 TextIoWrapper 进行文件的读写,但是还有个奇怪的问题,为什么用完 test_file 之后,我们不需要手动调用 test_file.close() 方法来关闭对象呢?
难道是系统会自动帮我们关闭 IO 对象吗?
好在 Python 是开源的,我们可以查看它的源码来验证我们的想法,在 Pycharm 中,直接点进去查看 open 方法的源码,但是却是个空实现,那真实的逻辑在哪里呢?
因为 Python 默认使用的是 CPython,因此肯定有地方是能找到源码的,经过搜索,发现 Python 的源码就在 github 上放着。怎么找到对应的源码呢?
可以直接使用关键字搜索,我是用 Pycharm 中看到的方法注释「Open file and return a stream」,结果搜索出很多相关的代码段,github 这个搜索功能貌似会将搜索语句分割成一个个关键字。没办法只能硬着头皮翻看头几页。
bingo!!找到如下图所示的内容:
其中一个是文件读写的 C 语言实现方式,另一个是 py 实现。我们就从 py 实现开始看,说不定就能找到我们需要的内容。还记得上面 with open 方法返回的是一个 TextIOWrapper 对象吧,我们直接在文件中搜索:「TextIOWrapper」,还真找到了,如下图所示:
接下来,我们需要找到它在哪里调用了 close 之类的方法来关闭文件读写的句柄。在 TextIOWrapper 类中,没找到有相关的操作,只能到它的父类 TextIOBase 中找找看。
在 TextIOBase 中只有常规的读和写的空实现,没找到我们需要的东西。只能再查看它的父类:IOBase,如果再找不到只能去 C 代码中碰碰运气。
功夫不负有心人啊,在 IOBase 中,我们看到了我们想找的代码,如下所示:
这个 enter 和 exit 方法又是怎么回事呢?网上查了下,它们和 Python With 上下文管理器有关系。所谓的上下文管理器是用来执行 with 语句时建立的运行时上下文的一个对象,通过调用对象的__ enter _和_ exit __ 方法来实现。
了解完 Python 的 With 上下文管理器怎么使用,接下来我们就尝试自己动手写一个上下文管理器,加深大家对它的理解。我们直接定义一个有异常情况发生的代码:
class TestWith(object):
def __init__(self):
print("初始化")
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("exit")
print(exc_type)
print(exc_val)
print(exc_tb)
return True
def operation(self):
print('todo something')
print(1 / 0)
print('exception happened')
if __name__ == '__main__':
# # 使用自定义的上下文管理器
with TestWith() as f:
f.operation()
执行上面代码的结果如下所示:
初始化
enter
todo something
exit
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x109bdcbe0>
奇怪了,为什么没有抛出异常,我们代码中明明使用了 1/0。这就是上下文管理器高明的地方,它能对异常进行捕获处理。可以注意到__exit__方法有三个参数:exc_type, exc_val,exc_tb,它们分别表示:
在上面例子中,它返回的值是就是等,不过需要注意当我们程序主逻辑没有报错时,这三个参数将都返回 None。
我总结了我认为的几个优势: