引言
对于正确的处理涉及到异常的资源管理时,需要使用try/finally
代码结构,这样的结构一多会导致整体代码结构 很臃肿繁琐,不易读、不美观,因此在 Python2.6 版本推出with
关键字。
with as
语句是Pyhton
提供的一种简化语法,适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的清理操作,释放资源。
<br/>
with操作文件
对于系统资源如文件、数据库连接、socket
而言,应用程序打开这些资源并执行完业务逻辑之后,必须做的一件事就是要关闭(释放)该资源。
比如 Python 程序打开一个文件,往文件中写内容,写完之后,就要关闭该文件,如果不关闭会出现什么情况呢?极端情况下会出现 Too many open files
的错误,因为系统允许你打开的最大文件数量是有限的。
同样,对于数据库,如果连接数过多而没有及时关闭的话,就可能会出现 Can not connect to MySQL server Too many connections
,因为数据库连接是一种非常昂贵的资源,不可能无限制的被创建。
在代码中经常会看见 with open(file) as f
对文件进行操作,其中 with
关键字到底有什么用处呢?让我们一起了解一下其底层原理。来看看如何正确关闭一个文件。
<br/>
普通版:
file = "test.txt"
def fun1():
"""
普通版
"""
f = open(file, "w")
f.write("hello python")
f.close
这样写有一个潜在的问题,如果在调用 write
的过程中,出现了异常进而导致后续代码无法继续执行,close
方法无法被正常调用,因此资源就会一直被该程序占用者释放。那么该如何改进代码呢?
<br/>
进阶版:
file = "test.txt"
def fun2():
"""
异常处理
"""
try:
f = open(file, "w")
f.write("hello python")
except Exception as e:
print(e)
finally:
f.close()
改良版本的程序是对可能发生异常的代码处进行 try
捕获,使用 try/finally
语句,该语句表示如果在 try
代码块中程序出现了异常,后续代码就不再执行,而直接跳转到 except
代码块。而无论如何,finally
块的代码最终都会被执行。因此,只要把 close
放在 finally
代码中,文件就一定会关闭。
<br/>
高级版:
file = "test.txt"
def fun3():
"""
with 关键字
"""
with open(file, "w") as f:
f.write("hello python")
一种更加简洁、优雅的方式就是用 with
关键字。open
方法的返回值赋值给变量 f,当离开 with 代码块的时候,系统会自动调用 f.close()
方法, with 的作用和使用 try/finally
语句是一样的。那么它的实现原理是什么?在讲 with 的原理前要涉及到另外一个概念,就是 上下文管理器(Context Manager)。
<br/>
上下文管理器
什么是上下文?
上下文在不同的地方表示不同的含义,要感性理解。在编程中context
上下文其实说白了就是环境。例如 一个 APP 应用,在切换界面的时候,要保存你是在哪个屏幕跳过来的等等信息,以便你点击返回的时候能正确跳回,如果不存肯定就无法正确跳回了。
再比如线程、协程进行任务切换时,程序怎么能知道切换到另一个任务,是从头开始执行还是从中间呢?其上下文就起到作用,就是任务本身会对其环境进行保存,做到哪里了,做了多少,各种状态都会标识记录,从而形成了上下文环境,因此在切换时根据每个任务的上下文环境,继续执行,从而达到多任务。
<br/>
上下文管理器
任何类实现了__enter__()
和__exit__()
方法的对象都可称之为上下文管理器。上下文管理器对象可以使用
with
关键字。
file = "test.txt"
def fun3():
"""
with 关键字
"""
with open(file, "w") as f:
# with 代码块
f.write("hello python")
print("with 语句结束")
__enter__(self)
:进入上下文管理器自动调用的方法,该方法会在 with as 代码块 执行之前执行。如果 with 语句有 as子句,那么该方法的返回值会被赋值给 as 子句后的变量;该方法可以返回多个值,因此在 as 子句后面也可以指定多个变量(多个变量必须由()括起来组成元组)。__exit__(self, exc_type, exc_value, exc_traceback)
:退出上下文管理器自动调用的方法。该方法会在 with as 代码块 执行之后执行。如果 with as 代码块成功执行结束,程序自动调用该方法,调用该方法的三个参数都为 None,如果 with as 代码块 因为 异常而中止,程序也自动调用该方法,使用sys.exc_info
得到的异常信息将作为调用该方法的参数。
<br/>
基于类的上下文管理器
我们可以模拟实现一个自己的文件类,让该类实现 __enter__()
和 __exit__()
方法。
"""
with关键字的实现原理
上下文管理器
"""
# 基于类实现上下文管理器
class File(object):
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""
进入with as 语句的时候被with调用
返回值作为 as 后面的变量
"""
print("__enter__ called")
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_value, exc_traceback):
"""
离开with语句的时候被with调用
"""
print("__exit__ called")
print("exc_type: ", exc_type)
print("exc_value: ", exc_value)
print("exc_traceback: ", exc_traceback)
self.file.close()
print("文件关闭操作")
def main():
with File("test.txt", "w") as f:
print("with 代码块")
f.write("hello python1")
f.write("hello python2")
# a = 1 / 0
f.write("hello python3")
print("with 语句结束")
if __name__ == '__main__':
main()
__enter__()
方法返回资源对象,这里就是你将要打开的那个文件对象,__exit__()
方法处理一些清除工作。
因为 File 类实现了上下文管理器,现在就可以使用 with 语句了。其运行结果如下:
__enter__ called
with 代码块
__exit__ called
exc_type: None
exc_value: None
exc_traceback: None
文件关闭操作
with 语句结束
<br/>
把 除0异常 的代码注释去了看看结果
__enter__ called
with 代码块
__exit__ called
exc_type: <class 'ZeroDivisionError'>
exc_value: division by zero
exc_traceback: <traceback object at 0x0000021F0780BCC8>
文件关闭操作
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\pycode\withdemo.py", line 113, in <module>
main()
File "C:\Users\Administrator\Desktop\pycode\withdemo.py", line 106, in main
a = 1 / 0
ZeroDivisionError: division by zero
<br/>
这样,你就无需显示地调用 close
方法了,由系统自动去调用,哪怕中间遇到异常 close
方法也会被调用。
<br/>
基于contextmanager装饰器
Python 在 contextlib
模块中还提供了一个 contextmanager
的装饰器,更进一步简化了上下文管理器的实现方式。通过 yield
将函数分割成两部分,yield 之前的语句在 __enter__
方法中执行,yield 之后的语句在 __exit__
方法中执行。紧跟在 yield
后面的值是函数的返回值。
# 基于contextmanager装饰器
from contextlib import contextmanager
@contextmanager
def file_manager(name, mode):
print("file_manager() called")
try:
f = open(name, mode)
yield f
finally:
f.close()
print("文件关闭操作")
调用
with file_manager('test.txt', 'w') as f:
print("with 代码块")
f.write('hello world')
# a = 1 / 0
print("with 语句结束")
结果
file_manager() called
with 代码块
文件关闭操作
with 语句结束
<br/>
总结
Python 提供了 with
语法用于简化资源操作的后续清除操作,是 try/finally
的替代方法,实现原理建立在上下文管理器之上。此外,Python 还提供了一个 contextmanager
装饰器,更进一步简化上下文管理器的实现方式。基于类和基于 contextmanager
的上下文管理器,这两者在功能上是一致的。只不过,基于类的上下文管理器 更加灵活,适用于大型的系统开发,而基于 contextmanager
的上下文管理器 更加方便、简洁,适用于中小型程序。
无论使用哪一种,不要忘记在方法 __exit__()
或者是 finally
块中释放资源,这一点尤其重要。
<br/>
源代码
源代码已上传到 Gitee
PythonKnowledge: Python知识宝库,欢迎大家来访。
✍ 码字不易,还望各位大侠多多支持❤️。
<br/>
公众号
新建文件夹X
大自然用数百亿年创造出我们现实世界,而程序员用几百年创造出一个完全不同的虚拟世界。我们用键盘敲出一砖一瓦,用大脑构建一切。人们把1000视为权威,我们反其道行之,捍卫1024的地位。我们不是键盘侠,我们只是平凡世界中不凡的缔造者 。