Python进阶——什么是上下文管理器?
在 Python 开发中,我们经常会使用到 with
语法块,例如在读写文件时,保证文件描述符的正确关闭,避免资源泄露问题。
你有没有思考过, with
背后是如何实现的?我们常常听到的上下文管理器究竟是什么?
这篇文章我们就来学习一下 Python 上下文管理器,以及 with
的运行原理。
with语法块
在讲解 with
语法之前,我们先来看一下不使用 with
的代码如何写?
我们在操作一个文件时,代码可以这么写:
# 打开文件
f = open('file.txt')
for line in f:
# 读取文件内容 执行其他操作
# do_something...
# 关闭文件
f.close()
复制代码
这个例子非常简单,就是打开一个文件,然后读取文件中的内容,最后关闭文件释放资源。
但是,代码这么写会有一个问题:在打开文件后,如果要对读取到的内容进行其他操作,在这操作期间发生了异常,这就会导致文件句柄无法被释放,进而导致资源的泄露。
如何解决这个问题?
也很简单,我们使用 try ... finally
来优化代码:
# 打开文件
f = open('file.txt')
try:
for line in f:
# 读取文件内容 执行其他操作
# do_something...
finally:
# 保证关闭文件
f.close()
复制代码
这么写的好处是,在读取文件内容和操作期间,无论是否发生异常,都可以保证最后能释放文件资源。
但这么优化,代码结构会变得很繁琐,每次都要给代码逻辑增加 try ... finally
才可以,可读性变得很差。
针对这种情况,我们就可以使用 with
语法块来解决这个问题:
with open('file.txt') as f:
for line in f:
# do_something...
复制代码
使用 with
语法块可以完成之前相同的功能,而且这么写的好处是,代码结构变得非常清晰,可读性也很好。
明白了 with
的作用,那么 with
究竟是如何运行的呢?
上下文管理器
首先,我们来看一下 with
的语法格式:
with context_expression [as target(s)]:
with-body
复制代码
with
语法非常简单,我们只需要 with
一个表达式,然后就可以执行自定义的业务逻辑。
但是,with
后面的表达式是可以任意写的吗?
答案是否定的。要想使用 with
语法块,with
后面的的对象需要实现「上下文管理器协议」。
什么是「上下文管理器协议」?
一个类在 Python 中,只要实现以下方法,就实现了「上下文管理器协议」:
__enter__
:在进入with
语法块之前调用,返回值会赋值给with
的target
__exit__
:在退出with
语法块时调用,一般用作异常处理
我们来看实现了这 2 个方法的例子:
class TestContext:
def __enter__(self):
print('__enter__')
return 1
def __exit__(self, exc_type, exc_value, exc_tb):
print('exc_type: %s' % exc_type)
print('exc_value: %s' % exc_value)
print('exc_tb: %s' % exc_tb)
with TestContext() as t:
print('t: %s' % t)
# Output:
# __enter__
# t: 1
# exc_type: None
# exc_value: None
# exc_tb: None
复制代码
在这个例子中,我们定义了 TestContext
类,它分别实现了 __enter__
和 __exit__
方法。
这样一来,我们就可以把 TestContext
当做一个「上下文管理器」来使用,也就是通过 with TestContext() as t
方式来执行。
从输出结果我们可以看到,具体的执行流程如下:
__enter__
在进入with
语句块之前被调用,这个方法的返回值赋给了with
后的t
变量__exit__
在执行完with
语句块之后被调用
如果在 with
语句块内发生了异常,那么 __exit__
方法可以拿到关于异常的详细信息:
exc_type
:异常类型exc_value
:异常对象exc_tb
:异常堆栈信息
我们来看一个发生异常的例子,观察 __exit__
方法拿到的异常信息是怎样的:
with TestContext() as t:
# 这里会发生异常
a = 1 / 0
print('t: %s' % t)
# Output:
# __enter__
# exc_type: <type 'exceptions.ZeroDivisionError'>
# exc_value: integer division or modulo by zero
# exc_tb: <traceback object at 0x10d66dd88>
# Traceback (most recent call last):
# File "base.py", line 16, in <module>
# a = 1 / 0
# ZeroDivisionError: integer division or modulo by zero
复制代码
从输出结果我们可以看到,当 with
语法块内发生异常后,__exit__
输出了这个异常的详细信息,其中包括异常类型、异常对象、异常堆栈。
如果我们需要对异常做特殊处理,就可以在这个方法中实现自定义逻辑。
回到最开始我们讲的,使用 with
读取文件的例子。之所以 with
能够自动关闭文件资源,就是因为内置的文件对象实现了「上下文管理器协议」,这个文件对象的 __enter__
方法返回了文件句柄,并且在 __exit__
中实现了文件资源的关闭,另外,当 with
语法块内有异常发生时,会抛出异常给调用者。
伪代码可以这么写:
class File:
def __enter__(self):
return file_obj
def __exit__(self, exc_type, exc_value, exc_tb):
# with 退出时释放文件资源
file_obj.close()
# 如果 with 内有异常发生 抛出异常
if exc_type is not None:
raise exception
复制代码
这里我们小结一下,通过对 with
的学习,我们了解到,with
非常适合用需要对于上下文处理的场景,例如操作文件、Socket,这些场景都需要在执行完业务逻辑后,释放资源。
contextlib模块
对于需要上下文管理的场景,除了自己实现 __enter__
和 __exit__
之外,还有更简单的方式来做吗?
答案是肯定的。我们可以使用 Python 标准库提供的 contextlib
模块,来简化我们的代码。
使用 contextlib
模块,我们可以把上下文管理器当成一个「装饰器」来使用。
其中,contextlib
模块提供了 contextmanager
装饰器和 closing
方法。
下面我们通过例子来看一下它们是如何使用的。
contextmanager装饰器
我们先来看 contextmanager
装饰器的使用:
from contextlib import contextmanager
@contextmanager
def test():
print('before')
yield 'hello'
print('after')
with test() as t:
print(t)
# Output:
# before
# hello
# after
复制代码
在这个例子中,我们使用 contextmanager
装饰器和 yield
配合,实现了和前面上下文管理器相同的功能,它的执行流程如下:
- 执行
test()
方法,先打印出before
- 执行
yield 'hello'
,test
方法返回,hello
返回值会赋值给with
语句块的t
变量 - 执行
with
语句块内的逻辑,打印出t
的值hello
- 又回到
test
方法中,执行yield
后面的逻辑,打印出after
这样一来,当我们使用这个 contextmanager
装饰器后,就不用再写一个类来实现上下文管理协议,只需要用一个方法装饰对应的方法,就可以实现相同的功能。
不过有一点需要我们注意:在使用 contextmanager
装饰器时,如果被装饰的方法内发生了异常,那么我们需要在自己的方法中进行异常处理,否则将不会执行 yield
之后的逻辑。
@contextmanager
def test():
print('before')
try:
yield 'hello'
# 这里发生异常 必须自己处理异常逻辑 否则不会向下执行
a = 1 / 0
finally:
print('after')
with test() as t:
print(t)
复制代码
closing方法
我们再来看 contextlib
提供的 closing
方法如何使用。
closing
主要用在已经实现 close
方法的资源对象上:
from contextlib import closing
class Test():
# 定义了 close 方法才可以使用 closing 装饰器
def close(self):
print('closed')
# with 块执行结束后 自动执行 close 方法
with closing(Test()):
print('do something')
# Output:
# do something
# closed
复制代码
从执行结果我们可以看到,with
语句块执行结束后,会自动调用 Test
实例的 close
方法。
所以,对于需要自定义关闭资源的场景,我们可以使用这个方法配合 with
来完成。
contextlib的实现
学习完了 contextlib
模块的使用,最后我们来看一下 contextlib
模块是究竟是如何实现的?
contextlib
模块相关的源码如下:
class _GeneratorContextManagerBase:
def __init__(self, func, args, kwds):
# 接收一个生成器对象 (方法内包含 yield 的方法就是一个生成器)
self.gen = func(*args, **kwds)
self.func, self.args, self.kwds = func, args, kwds
doc = getattr(func, "__doc__", None)
if doc is None:
doc = type(self).__doc__
self.__doc__ = doc
class _GeneratorContextManager(_GeneratorContextManagerBase,
AbstractContextManager,
ContextDecorator):
def __enter__(self):
try:
# 执行生成器 代码会运行生成器方法的 yield 处
return next(self.gen)
except StopIteration:
raise RuntimeError("generator didn't yield") from None
def __exit__(self, type, value, traceback):
# with 内没有异常发生
if type is None:
try:
# 继续执行生成器
next(self.gen)
except StopIteration:
return False
else:
raise RuntimeError("generator didn't stop")
# with 内发生了异常
else:
if value is None:
value = type()
try:
# 抛出异常
self.gen.throw(type, value, traceback)
except StopIteration as exc:
return exc is not value
except RuntimeError as exc:
if exc is value:
return False
if type is StopIteration and exc.__cause__ is value:
return False
raise
except:
if sys.exc_info()[1] is value:
return False
raise
raise RuntimeError("generator didn't stop after throw()")
def contextmanager(func):
@wraps(func)
def helper(*args, **kwds):
return _GeneratorContextManager(func, args, kwds)
return helper
class closing(AbstractContextManager):
def __init__(self, thing):
self.thing = thing
def __enter__(self):
return self.thing
def __exit__(self, *exc_info):
self.thing.close()
复制代码
源码中我已经添加好了注释,你可以详细看一下。
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
方法。
使用场景
学习完了上下文管理器,那么它们具体会用在什么场景呢?
下面我举几个常用的例子来演示下,你可以参考一下结合自己的场景使用。
Redis分布式锁
from contextlib import contextmanager
@contextmanager
def lock(redis, lock_key, expire):
try:
locked = redis.set(lock_key, 'locked', expire)
yield locked
finally:
redis.delete(lock_key)
# 业务调用 with 代码块执行结束后 自动释放锁资源
with lock(redis, 'locked', 3) as locked:
if not locked:
return
# do something ...
复制代码
在这个例子中,我们实现了 lock
方法,用于在 Redis 上申请一个分布式锁,然后使用 contextmanager
装饰器装饰了这个方法。
之后我们业务在调用 lock
方法时,就可以使用 with
语法块了。
with
语法块的第一步,首先判断是否申请到了分布式锁,如果申请失败,则业务逻辑直接返回。如果申请成功,则执行具体的业务逻辑,当业务逻辑执行完成后,with
退出时会自动释放分布式锁,就不需要我们每次都手动释放锁了。
Redis事物和管道
from contextlib import contextmanager
@contextmanager
def pipeline(redis):
pipe = redis.pipeline()
try:
yield pipe
pipe.execute()
except Exception as exc:
pipe.reset()
# 业务调用 with 代码块执行结束后 自动执行 execute 方法
with pipeline(redis) as pipe:
pipe.set('key1', 'a', 30)
pipe.zadd('key2', 'a', 1)
pipe.sadd('key3', 'a')
复制代码
在这个例子中,我们定义了 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
Python进阶——什么是上下文管理器?的更多相关文章
- Python进阶(上下文管理器与with语句)
/*上下文管理器必须有__enter__和__exit__方法*/ class MyResource: def __enter__(self): print('链接资源') return self / ...
- python的上下文管理器-1
reference:https://zhuanlan.zhihu.com/p/26487659 来看看如何正确关闭一个文件. 普通版: def m1(): f = open("output. ...
- python2.7高级编程 笔记一(Python中的with语句与上下文管理器学习总结)
0.关于上下文管理器上下文管理器是可以在with语句中使用,拥有__enter__和__exit__方法的对象. with manager as var: do_something(var) 相当于以 ...
- 翻译《Writing Idiomatic Python》(五):类、上下文管理器、生成器
原书参考:http://www.jeffknupp.com/blog/2012/10/04/writing-idiomatic-python/ 上一篇:翻译<Writing Idiomatic ...
- python学习笔记4(对象/引用;多范式; 上下文管理器)
### Python的强大很大一部分原因在于,它提供有很多已经写好的,可以现成用的对象 21. 动态类型:对象/引用 对象和引用: 对象是储存在内存中的实体,对象名只是指向这一对象的引用(refere ...
- Python深入02 上下文管理器
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 上下文管理器(context manager)是Python2.5开始支持的一种语 ...
- python 上下文管理器
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 上下文管理器(context manager)是Python2.5开始支持的一种语 ...
- 【Python学习笔记】with语句与上下文管理器
with语句 上下文管理器 contextlib模块 参考引用 with语句 with语句时在Python2.6中出现的新语句.在Python2.6以前,要正确的处理涉及到异常的资源管理时,需要使用t ...
- 流畅python学习笔记:第十五章:上下文管理器
在开始本章之前,我们首先来谈谈try-excep..final模块.在Python中,进行异常保护的最多就是用try..except..final.首先来看下下面的代码.进行一个简单的除法运算.为了防 ...
随机推荐
- 【ES6】ES6入门笔记
1.概要 - ECMAScript2015(ES6)是Javascript最标准的语法式样,是在2015年6月由Ecma国籍组织公布的最新版本,现在已经被多个领域和浏览器所广泛采纳和使用. 2.学习网 ...
- 洛谷 P1360 [USACO07MAR]Gold Balanced Lineup G (前缀和+思维)
P1360 [USACO07MAR]Gold Balanced Lineup G (前缀和+思维) 前言 题目链接 本题作为一道Stl练习题来说,还是非常不错的,解决的思维比较巧妙 算是一道不错的题 ...
- mq内存映射
MappedFileQueue的封装 MappedFileQueue是MappedFile的管理容器,MappedFileQueue是对存储目录的封装. 查找MappedFile: 1.根据时间戳来查 ...
- 7-1 Hashing
The task of this problem is simple: insert a sequence of distinct positive integers into a hash tabl ...
- win10 下安装 ubuntu 子系统的完全指北
最近在搞 C++ 相关的东西,因为在 Linux 下开发会比较流畅舒适,而公司配的电脑都是 windows 的,之前都是在 vmware 中安装个 ubuntu 虚拟机,但这种有时候比有点卡顿.所以今 ...
- Kubernetes K8S之固定节点nodeName和nodeSelector调度详解
Kubernetes K8S之固定节点nodeName和nodeSelector调度详解与示例 主机配置规划 服务器名称(hostname) 系统版本 配置 内网IP 外网IP(模拟) k8s-mas ...
- Jmeter代理服务器录制脚本--浏览器拦截访问链接
在 Jmeter性能测试的过程中您是否会遇到代理服务器无法打开浏览器,无法录制脚本的情况呢? 在测试过程中,我也遇到过这样的问题,希望能帮到正在找寻答案的你.... Jmeter录制脚本时,跟http ...
- moviepy用VideoFileClip加载视频时报UnicodeDecodeError: codec cant decode ,No mapping character 错误
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 昨天处理视频时出现了解码错误,通过修改ffmpeg ...
- 第15.5节 PyQt的历史沿革介绍
当朋友向我推荐PyQt时,老猿才知道有这样一个在Python下的开源的图形界面开发工具,当准备学习PyQt安装时,发现要安装sip.PyQt.PyQt-tools,然后还要进行相关配置.老猿很好奇为什 ...
- ADF 第一篇:Azure Data Factory介绍
Azure Data Factory(简写 ADF)是Azure的云ETL服务,简单的说,就是云上的SSIS.ADF是基于云的ETL,用于数据集成和数据转换,不需要代码,直接通过UI(code-fre ...