在 Python 开发中,咱们常常会使用到 with
语法块,例如在读写文件时,保证文件描述符的正确关闭,避免资源泄露问题。html
你有没有思考过, with
背后是如何实现的?咱们经常听到的上下文管理器到底是什么?python
这篇文章咱们就来学习一下 Python 上下文管理器,以及 with
的运行原理。redis
在讲解 with
语法以前,咱们先来看一下不使用 with
的代码如何写?express
咱们在操做一个文件时,代码能够这么写:app
这个例子很是简单,就是打开一个文件,而后读取文件中的内容,最后关闭文件释放资源。分布式
可是,代码这么写会有一个问题:在打开文件后,若是要对读取到的内容进行其余操做,在这操做期间发生了异常,这就会致使文件句柄没法被释放,进而致使资源的泄露。学习
如何解决这个问题?优化
也很简单,咱们使用 try ... finally
来优化代码:url
这么写的好处是,在读取文件内容和操做期间,不管是否发生异常,均可以保证最后能释放文件资源。spa
但这么优化,代码结构会变得很繁琐,每次都要给代码逻辑增长 try ... finally
才能够,可读性变得不好。
针对这种状况,咱们就可使用 with
语法块来解决这个问题:
使用 with
语法块能够完成以前相同的功能,并且这么写的好处是,代码结构变得很是清晰,可读性也很好。
明白了 with
的做用,那么 with
到底是如何运行的呢?
首先,咱们来看一下 with
的语法格式:
with
语法很是简单,咱们只须要 with
一个表达式,而后就能够执行自定义的业务逻辑。
可是,with
后面的表达式是能够任意写的吗?
答案是否认的。要想使用 with
语法块,with
后面的的对象须要实现「上下文管理器协议」。
什么是「上下文管理器协议」?
一个类在 Python 中,只要实现如下方法,就实现了「上下文管理器协议」:
__enter__
:在进入 with
语法块以前调用,返回值会赋值给 with
的 target
__exit__
:在退出 with
语法块时调用,通常用做异常处理咱们来看实现了这 2 个方法的例子:
在这个例子中,咱们定义了 TestContext
类,它分别实现了 __enter__
和 __exit__
方法。
这样一来,咱们就能够把 TestContext
当作一个「上下文管理器」来使用,也就是经过 with TestContext() as t
方式来执行。
从输出结果咱们能够看到,具体的执行流程以下:
__enter__
在进入 with
语句块以前被调用,这个方法的返回值赋给了 with
后的 t
变量__exit__
在执行完 with
语句块以后被调用若是在 with
语句块内发生了异常,那么 __exit__
方法能够拿到关于异常的详细信息:
exc_type
:异常类型exc_value
:异常对象exc_tb
:异常堆栈信息咱们来看一个发生异常的例子,观察 __exit__
方法拿到的异常信息是怎样的:
从输出结果咱们能够看到,当 with
语法块内发生异常后,__exit__
输出了这个异常的详细信息,其中包括异常类型、异常对象、异常堆栈。
若是咱们须要对异常作特殊处理,就能够在这个方法中实现自定义逻辑。
回到最开始咱们讲的,使用 with
读取文件的例子。之因此 with
可以自动关闭文件资源,就是由于内置的文件对象实现了「上下文管理器协议」,这个文件对象的 __enter__
方法返回了文件句柄,而且在 __exit__
中实现了文件资源的关闭,另外,当 with
语法块内有异常发生时,会抛出异常给调用者。
伪代码能够这么写:
这里咱们小结一下,经过对 with
的学习,咱们了解到,with
很是适合用须要对于上下文处理的场景,例如操做文件、Socket,这些场景都须要在执行完业务逻辑后,释放资源。
对于须要上下文管理的场景,除了本身实现 __enter__
和 __exit__
以外,还有更简单的方式来作吗?
答案是确定的。咱们可使用 Python 标准库提供的 contextlib
模块,来简化咱们的代码。
使用 contextlib
模块,咱们能够把上下文管理器当成一个「装饰器」来使用。
其中,contextlib
模块提供了 contextmanager
装饰器和 closing
方法。
下面咱们经过例子来看一下它们是如何使用的。
咱们先来看 contextmanager
装饰器的使用:
在这个例子中,咱们使用 contextmanager
装饰器和 yield
配合,实现了和前面上下文管理器相同的功能,它的执行流程以下:
test()
方法,先打印出 before
yield 'hello'
,test
方法返回,hello
返回值会赋值给 with
语句块的 t
变量with
语句块内的逻辑,打印出 t
的值 hello
test
方法中,执行 yield
后面的逻辑,打印出 after
这样一来,当咱们使用这个 contextmanager
装饰器后,就不用再写一个类来实现上下文管理协议,只须要用一个方法装饰对应的方法,就能够实现相同的功能。
不过有一点须要咱们注意:在使用 contextmanager
装饰器时,若是被装饰的方法内发生了异常,那么咱们须要在本身的方法中进行异常处理,不然将不会执行 yield
以后的逻辑。
咱们再来看 contextlib
提供的 closing
方法如何使用。
closing
主要用在已经实现 close
方法的资源对象上:
从执行结果咱们能够看到,with
语句块执行结束后,会自动调用 Test
实例的 close
方法。
因此,对于须要自定义关闭资源的场景,咱们可使用这个方法配合 with
来完成。
学习完了 contextlib
模块的使用,最后咱们来看一下 contextlib
模块是到底是如何实现的?
contextlib
模块相关的源码以下:
源码中我已经添加好了注释,你能够详细看一下。
contextlib
源码中逻辑其实比较简单,其中 contextmanager
装饰器实现逻辑以下:
_GeneratorContextManager
类,构造方法接受了一个生成器 gen
__enter__
和 __exit__
with
时会进入到 __enter__
方法,而后执行这个生成器,执行时会运行到 with
语法块内的 yield
处__enter__
返回 yield
的结果with
语法块没有发生异常,with
执行结束后,会进入到 __exit__
方法,再次执行生成器,这时会运行 yield
以后的代码逻辑with
语法块发生了异常,__exit__
会把这个异常经过生成器,传入到 with
语法块内,也就是把异常抛给调用者再来看 closing
的实现,closing
方法就是在 __exit__
方法中调用了自定义对象的 close
,这样当 with
结束后就会执行咱们定义的 close
方法。
学习完了上下文管理器,那么它们具体会用在什么场景呢?
下面我举几个经常使用的例子来演示下,你能够参考一下结合本身的场景使用。
在这个例子中,咱们实现了 lock
方法,用于在 Redis 上申请一个分布式锁,而后使用 contextmanager
装饰器装饰了这个方法。
以后咱们业务在调用 lock
方法时,就可使用 with
语法块了。
with
语法块的第一步,首先判断是否申请到了分布式锁,若是申请失败,则业务逻辑直接返回。若是申请成功,则执行具体的业务逻辑,当业务逻辑执行完成后,with
退出时会自动释放分布式锁,就不须要咱们每次都手动释放锁了。
在这个例子中,咱们定义了 pipeline
方法,并使用装饰器 contextmanager
让它变成了一个上下文管理器。
以后在调用 with pipeline(redis) as pipe
时,就能够开启一个事物和管道,而后在 with
语法块内向这个管道中添加命令,最后 with
退出时会自动执行 pipeline
的 execute
方法,把这些命令批量发送给 Redis 服务端。
若是在执行命令时发生了异常,则会自动调用 pipeline
的 reset
方法,放弃这个事物的执行。
总结一下,这篇文章咱们主要介绍了 Python 上下文管理器的使用及实现。
首先咱们介绍了不使用 with
和使用 with
操做文件的代码差别,而后了解到使用 with
可让咱们的代码结构更加简洁。以后咱们探究了 with
的实现原理,只要实现 __enter__
和 __exit__
方法的实例,就能够配合 with
语法块来使用。
以后咱们介绍了 Python 标准库的 contextlib
模块,它提供了实现上下文管理更好的使用方式,咱们可使用 contextmanager
装饰器和 closing
方法来操做咱们的资源。
最后我举了两个例子,来演示上下文管理器的具体使用场景,例如在 Redis 中使用分布式锁和事物管道,用上下文管理器帮咱们管理资源,执行前置和后置逻辑。
因此,若是咱们在开发中把操做资源的前置和后置逻辑,经过上下文管理器来实现,那么咱们的代码结构和可维护性也会有所提升,推荐使用起来。
想学习更多关于python的知识能够加我QQ:2955637827