我们在Python中对于with的语句应该是不陌生的,特别是在文件的输入输出操作中,那在具体的使用过程中,是有什么引伸的含义呢?与之密切相关的上下文管理器(context manager)又是什么呢?

什么是上下文管理器

在任何一种编程语言里,文件的输入输出、数据库的建立连接和断开等操作,都是很常见的资源管理操作。但是资源是有限的,在写程序的时候,我们必须保证这些资源在使用后得到释放,不然就容易造成资源泄漏,轻者系统处理缓慢,重则系统崩溃。

我们看一个例子:

for i in range(100000000):
f = open('test.txt','w')
f.write('hello')

我们在循环里打开了100000000个文件,但是在使用完毕后没有进行关闭操作,一运行代码,就报错了。

这就是一个典型的资源泄漏的案例,因为程序中同时打开了太多的文件,占用了太多的资源,造成崩溃。

为了解决这个问题,不同的编程语言都引入了不同的机制,在Python中,对应的解决方法就是上下文管理器(context manager)。上下文管理器能够自动分配资源并释放资源,其中最典型的应用就是with语句,所以上面的代码应该用这种方式来写

for x in range(100000000):
with open('test.txt','w') as f:
f.write('hello world')

这样,我们每次打开文件“test.txt"并写入字符以后,这个文件就会自动关闭,相应的资源也就得以释放,可以防止资源泄漏。当然,with的语句也可以用下面的方式来表示

f = open('test.txt','w')
try:
f.write('hello world')
finally:
f.close()

这里一定要注意finally的程序段,哪怕在写入的时候发生了异常,他也可以保证文件最终被关闭。不过于with想比较,就显得比较冗余了,并且还容易忽略finally,所以我们平时更倾向于使用with语句。

另外一种很典型的例子,就是Python中的线程锁(threrading.lock类),比如我们想要获得一个锁,执行相应的操作以后再将其释放,那么代码就应该是这样的

import threading
some_lock = threading.Lock()
some_lock.acquire()
try:
pass
finally:
some_lock.release()

而与其对应的with语句就非常简洁了

import threading
some_lock = threading.Lock()
with some_lock:
pass

从上面两个例子可以发现,使用with语句,可以大大的简化代码结构,有效的避免资源泄漏的发生。


上下文管理器的实现

基于类的上下文管理器 

了接了上下文管理的概念和优点以后,我们就通过下面的例子,看看上下文管理器的原理,高清他的内部实现。我们在这里定义一个上下文管理类FileManager,来模拟Python的打开、关闭文件的操作

class FileManager():
def __init__(self,name,mode):
print('call __init__ method')
self.name = name
self.mode = mode
self.file = None def __enter__(self):
print('calling __enter__ method')
self.file = open(self.name,self.mode)
return self.file
def __exit__(self,exc_type,exc_val,exc_tb):
print('call __exit__ method')
if self.file:
self.file.close() with FileManager('test.txt','w') as f:
print('ready to write to file')
f.write('hello world') ##########输出##########
call __init__ method
calling __enter__ method
ready to write to file
call __exit__ method

特别注意:当我们用类来创建上下文管理器的时候,必须保证这个类包括下面两个方法:

__enter__()
__exit__()

并且enter方法还要返回需要被管理的资源,方法exit里通常会存在一些释放、清理资源的操作,比如上面这段代码里的关闭文件等。

而当我们用with语句来执行上面这个上下文管理器的时候,会发生下面四个步骤:

1.构造方法__init__()会被调用,程序初始化对象FileManager,使得文件名和操作方式被传入。

2.方法__enter__()被调用,文件被以写入的模式打开,并且返回FileManager对戏那个赋值给变量f

3.字符串被写入文件

4.方法__exit__()被调用,关闭之前打开的文件流。

所以就有了上面列出的输出结果。

另外我们可以看到exit函数里传递了几个参数——exc_type.exc_val,exc_tb,分别表示exception_type,exception_value和traceback。当我们执行含有上下文管理器的with语句的时候,如果有异常抛出,异常的信息就会被包含在上面三个参数中,传给__exit__()函数。

因此,如果我们需要处理一些异常,可以在__exit__()函数中添加相应的代码

    def __exit__(self,exc_type,exc_val,exc_tb):
print('call __exit__ method')
if exc_type:
print(f'exc_type:{exc_type}')
print(f'exc_value:{exc_val}')
print(f'exc_traceback:{exc_tb}')
print('exception handled')
return True
if self.file:
self.file.close() with FileManager('test.txt','w') as f:
raise Exception('exception raised').with_traceback(None)

在修改了exit()方法以后我们在with语句中用raise手动抛出异常,我们可以看到代码有下面的输出

call __init__ method
calling __enter__ method
call __exit__ method
exc_type:<class 'Exception'>
exc_value:exception raised
exc_traceback:<traceback object at 0x000001AA82C8FF48>
exception handled

要注意的是,如果exit函数如果没有返回True,呢么异常仍然会被抛出。如果我们确定了异常已经被处理,那么在exit最后要加上True的返回值。


同样的,在数据库的连接等操作上,也常常使用上下文管理器来表示,下面是个简化的代码

class DBConnectionManager():
def __init__(self,hostname,port):
self.hostname = hostname
self.port = port
self.connection = None def __enter__(self):
self.connection = DBClient(self.hostname,self.port)
return self def __exit__(self,exc_type,exc_val,exc_tb):
self.connection.close() with DBConnectionManager('localhost','') as db_client:
pass

代码的具体含义和上面的例子类似,就不再详细说明了。只要我们写完了DBConnectionManager这个类以后,在每次建立数据库连接时,只要简单的利用with语句就可以了,并不要关系数据库的关闭、异常等,大大的提高了开发效率。

基于生成器的上下文管理器

上面那种基于类的上下文管理器在Python中利用非常广泛,我们在很多项目中都可以看得到,不过Python中的上下文管理器不仅仅局限于此,畜类基于类,它还可以基于生成器实现,我们看看下面的例子:

我们用一个装饰器contextlib.contextmanager来定义自己所需要的基于生成器的上下文管理器,用以支持with语句。同样我们用前面的FileManager来演示

from contextlib import contextmanager

@contextmanager
def file_manager(name,mode):
try:
f = open(name,mode)
yield f
finally:
f.close() with file_manager('test.txt','w') as f:
f.write('hello world')

这段代码中,函数file_manager()是一个生成器,当我们执行with语句的时候,便会打开文件,并返回文件对象f,当with语句执行完毕以后,finally代码段中的关闭操作就会执行。

可以看到,使用基于生成器的上下文管理器的时候,我们不用再定义__enter__()和__exit__()两个函数,但是必须加上装饰器,这一点非常容易漏掉。

讲完这两种上下文管理器以后,我们要强调一点:不论是基于类的还是生成器的上下文管理器,两者在功能上是一样的,只不过有下面两点:

1.基于类的上下文管理器更加灵活,适用于大型的系统开发

2.基于生成器的上下文管理器更加方便、简洁,适用于中小型程序。

但是无论使用哪一种,我们一定要记得在exit函数或finally里写好释放资源的代码,这一点尤为重要。

总结

在这一章的开头我们通过一个简单的例子了解了资源泄漏的易发生的特性和其带来的后果,从而引入了上下文管理器这个概念:

  上下文管理器通常用在文件的IO操作和数据库的连接关闭等场景中,可以确保用过的资源得到迅速释放,有效提高了程序的安全性。

接着,我们通过自定义上下文管理器的示例,大致了解了上下文管理器工作的原理,并介绍基于类的上下文管理器和基于生成器的上下文管理器:两者功能相同,具体使用哪个要根据场景来选择。

另外,上下文管理器通常和with一起使用,大大提高了程序的简洁度,需要注意的是我们在使用with语句执行上下文操作的时候,一旦有异常抛出,异常的类型、值等拘役信息都会通过参数传递给__exit__()函数,我们可以自行定义相关的操作,而在对异常处理完毕以后,务必加上return True语句来保证程序的执行,否则仍然会抛出异常。

Python核心技术与实战——二一|巧用上下文管理器和with语句精简代码的更多相关文章

  1. 翻译《Writing Idiomatic Python》(五):类、上下文管理器、生成器

    原书参考:http://www.jeffknupp.com/blog/2012/10/04/writing-idiomatic-python/ 上一篇:翻译<Writing Idiomatic ...

  2. Python中的上下文管理器和with语句

    Python2.5之后引入了上下文管理器(context manager),算是Python的黑魔法之一,它用于规定某个对象的使用范围.本文是针对于该功能的思考总结. 为什么需要上下文管理器? 首先, ...

  3. Python高级笔记(八)with、上下文管理器

    1. 上下文管理器 __enter__()方法返回资源对象,__exit__()方法处理一些清除资源 如:系统资源:文件.数据库链接.Socket等这些资源执行完业务逻辑之后,必须要关闭资源 #!/u ...

  4. Python核心技术与实战——二十|assert的合理利用

    我们平时在看代码的时候,或多或少会看到过assert的存在,并且在有些code review也可以通过增加assert来使代码更加健壮.但是即便如此,assert还是很容易被人忽略,可是这个很不起眼的 ...

  5. Python核心技术与实战——二十|Python的垃圾回收机制

    今天要讲的是Python的垃圾回收机制 众所周知,我们现在的计算机都是图灵架构.图灵架构的本质,就是一条无限长的纸带,对应着我们的存储器.随着寄存器.异失性存储器(内存)和永久性存储器(硬盘)的出现, ...

  6. python上下文管理器及with语句

    with语句支持在一个叫上下文管理器的对象的控制下执行一系列语句,语法大概如下: with context as var: statements 其中的context必须是个上下文管理器,它实现了两个 ...

  7. Python进阶(上下文管理器与with语句)

    /*上下文管理器必须有__enter__和__exit__方法*/ class MyResource: def __enter__(self): print('链接资源') return self / ...

  8. Python核心技术与实战 笔记

    基础篇 Jupyter Notebook 优点 整合所有的资源 交互性编程体验 零成本重现结果 实践站点 Jupyter 官方 Google Research 提供的 Colab 环境 安装 运行 列 ...

  9. 【Python学习笔记】with语句与上下文管理器

    with语句 上下文管理器 contextlib模块 参考引用 with语句 with语句时在Python2.6中出现的新语句.在Python2.6以前,要正确的处理涉及到异常的资源管理时,需要使用t ...

随机推荐

  1. 图片和Base64字符串互转

    图片URL转成Base64字符串 /// <summary> /// 通过Url获取到Image格式的文件 /// </summary> /// <param name= ...

  2. 应用安全 - 工具|平台 - CDN - 使用|命令 - 汇总

    简介 用途 使用缓存适应高并发请求 功能 ()抗DDOS ()隐藏真实IP 全球DNS地址分布:http://www.ab173.com/dns/dns_world.php全球IP地址段分布:http ...

  3. vscode配置PHP Debug

    1.先在vscode中安装PHP Debug,在设置添加“php.validate.executablePath”项,选中对应版本的php.exe. "php.validate.execut ...

  4. Java十大bug之——包冲突

    找bug就像破案,有的bug简单,有的bug复杂,还有的bug隐藏的令人难以发现. 一个逻辑上看起来一切都正常,结果确有问题,且怎么分析都感觉自己写的没问题的情况——包冲突 遇到这个bug最开始没有任 ...

  5. spring5源码分析系列(一)——spring5框架模块

    spring总共大约20个模块,这些模块被整合在核心容器(Core Container).AOP和设备支持.数据访问及集成.Web.报文发送.Test 6个模块集合. 组成Spring框架的每个模块集 ...

  6. .Net Core 3.0使用Grpc进行远程过程调用

    因为.Net Core3.0已经把Grpc作为一等臣民了,作为爱好新技术的我,当然要尝鲜体验一下了,当然感觉是Grpc作为跨语言的产品做的相当好喽,比起Dubbo这种的,优势和劣势还是比较明显的. 我 ...

  7. pom.xml标签页名称

    pom.xml文件双击打开后,标签页显示的名称与<artifactId></artifactId>的内容相一致.

  8. AcWing登山

    这是2006北大举办的ACM的一道题. 题意为:给定景点海拔高度,队员们不去游览相同高度的景点,一开始往上爬,一但往下爬就不能再向上爬,求最多可以游览多少个景点.那么我们可以得到一个结论:以一个最高点 ...

  9. excel库中数据下载

    PHP实现EXCEL下载数据 <?php include("Classes/PHPExcel.php"); $exce=new PHPExcel(); $exce->s ...

  10. MYSQL中的UNION和UNION ALL

    SQL UNION 操作符 UNION 操作符用于合并两个或多个 SELECT 语句的结果集. 请注意,UNION 内部的 SELECT 语句必须拥有相同数量的列.列也必须拥有相似的数据类型.同时,每 ...