专栏文章 Python with 上下文管理器的用法和原理

布道师玄柯 · 2020年12月10日 · 1044 次阅读

正好前几天有小伙伴在「测试开发群」里问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 __ 方法来实现。

自定义一个 With 上下文管理器

了解完 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,它们分别表示:

  • exc_type:异常类型
  • exc_val:异常值
  • exc_tb:异常的堆栈信息

在上面例子中,它返回的值是就是等,不过需要注意当我们程序主逻辑没有报错时,这三个参数将都返回 None。

python 上下文管理器的优势

我总结了我认为的几个优势:

  • 它能让我们的代码变的简洁,比如:with open 读写文件时,我们不需要再去调用 close 方法,避免遗忘造成的各种问题。
  • 它能对异常进行捕获处理,其实也是让代码简洁了,我们不需要在代码中写很多 try...except。
  • 它能提高代码的复用率,比如:我们可以将一些常用的操作放到 init(初始化操作)和 exit(退出操作)方法中,这样方便其他人调用。

转载自:https://mp.weixin.qq.com/s?__biz=MzIzODU5ODUxMA==&mid=2247484148&idx=1&sn=fd0fd371ae3ec1f66877048cc8c5a530&chksm=e937a2f5de402be39680555f451543ffae47b16184393bc814fef512ce6248b57ebc2a926674&token=1926412886&lang=zh_CN#rd

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册