首页 > 技术文章 > python上下文管理器

laozhanghahaha 2020-02-16 20:13 原文

资源管理

在任何编程语言中,对于资源的使用诸如文件操作或数据库连接等非常普遍。 但是这些资源是有限的,因此我们要确保使用这些资源后及时释放他们, 否则将导致资源泄漏,甚至会使系统变慢或崩溃。 在Python中,可以通过使用上下文管理器来实现对于资源的分配和及时释放,这有助于对资源的正确处理。 执行文件操作的最常见方式是使用with关键字,假设你有两个相关的操作要成对执行,并且这两个相关操作之间还有一段代码,我们就可以使用上下文管理器实现这个目的。 基本语法格式为:

with EXPR as VAR:
    BLOCK

以文件操作为例,当我们打开一个文件,就会消耗一个文件描述符.文件描述符属于有限的资源,因此一个进程一次只能打开有一定数量的文件,如果们打开了太多文件而且没有及时关闭就会造成文件描述符泄露.下面用代码来解释这个问题

>>> file_descriptors = []
>>> for x in range(100000):
...     file_descriptors.append(open('test.txt', 'w'))
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
OSError: [Errno 24] Too many open files: 'test.txt'

 

 

使用上写文管理器进行资源管理

如果代码块抛出异常,或者代码块包含具有多个返回路径的复杂算法,则在每个位置逐一关闭文件将变得很麻烦.在使用其他语言时,使用try-except-finally可以确保即使有异常也可以在使用后关闭文件资源.Python提供了一种更简单的方法来管理资源:上下文管理器. 使用with关键字例如:

with open("test.txt", 'w') as f: 
    f.write('Hello World!')

这段代码执行打开文件,写进数据,关闭文件操作.如果在写进数据的时候抛出错误,上下文管理器会尝试解决这个错误, 它与下边这段代码的作用是一样的.

file = open('test.txt', 'w')
try:
    file.write('Hello World!')
finally:
    file.close()

比较这两段代码我们就会发现使用with使代码更加精简,但使用with的最主要好处是他能够确保关闭文件,且不需要考虑嵌套块退出的方式.

上下文管理器的常见用例是占用和释放资源以及关闭打开的文件.接下来我们尝试自己实现一个上下文管理器,从而对他有一个更好的理解

 

实现上下文管理器

 上下文管理器可以使用类或函数(带有装饰器)来编写,当我们用类实现上下文管理器时,要确保类中含有 __enter__() 和 __exit__()方法.  __enter__() 用来返回要被使用的资源,__exit__() 执行清除操作.接下来我们用两个例子解释如何用类创建上下文管理器.

>>> class ContextManager(): 
...     def __init__(self): 
...         print('init method called') 
...           
...     def __enter__(self): 
...         print('enter method called') 
...         return self
...       
...     def __exit__(self, exc_type, exc_value, exc_traceback): 
...         print('exit method called') 
... 
>>> with ContextManager() as manager: 
...     print('with statement block')
... 
init method called
enter method called
with statement block
exit method called

从输出结果中我们不难看出这段代码执行顺序如下

__init__()
__enter__()
code inside the with block
__exit__()

 

接下来以文件操作为例来解释如何用类来创建上下文管理器

>>> class FileManager(): 
...     def __init__(self, filename, mode): 
...         self.filename = filename 
...         self.mode = mode 
...         self.file = None
...           
...     def __enter__(self): 
...         self.file = open(self.filename, self.mode) 
...         return self.file
...       
...     def __exit__(self, exc_type, exc_value, exc_traceback): 
...         self.file.close() 
... 
>>> with FileManager('demo.txt', 'w') as f:
...     f.write('Hello World!')
... 
>>> print(f.closed) 
True
>>> 

 

执行上述代码,实际运行过程如下

  1. with语句把__exit__方法 存储在 FileManager中.
  2. 调用 FileManager类中的__enter__方法
  3. __enter__方法以写模式打开文件并返回这个FileManager对象
  4. 将__enter__方法的返回值赋值给变量f
  5. 利用.write()在文件中写入"Hello World"
  6. with调用__exit__方法
  7. __exit__关闭文件

 

异常处理

__exit__方法有三个参数:exc_type, exc_val, exc_tb。如果with下代码块发生异常并退出,那么分别对应异常的type、value 和 traceback。否则三个参数全为None。具体来说,如果在第四到第六步有异常抛出,Python会把异常的type、value 和 traceback传给__exit__方法.之后由__exit__方法决定如何关闭文件以及其他后续操作.实际运行过程如下

  1. with把异常的type、value 和 traceback传给__exit__方法
  2. __exit__处理异常
  3. 如果__exit__返回True则说明异常被成功处理
  4. 如果__exit__返回其他值,则异常会被with抛出

在我们自定义的代码中__exit__方法返回的是None,因此如果我们尝试访问一个不存在的函数,with语句则会抛出如下异常

>>> with FileManager('demo.txt', 'w') as f:
...     f.undefined_function('haha')
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: '_io.TextIOWrapper' object has no attribute 'undefined_function'
>>> 

修改__exit__方法来处理异常

>>> class FileManager(): 
...     def __init__(self, filename, mode): 
...         self.filename = filename 
...         self.mode = mode 
...         self.file = None
...           
...     def __enter__(self): 
...         self.file = open(self.filename, self.mode) 
...         return self.file
...        
...     def __exit__(self, exc_type, exc_value, exc_traceback): 
...         print("Exception has been handled")
...         self.file.close()
...         return True
... 
>>> with FileManager('demo.txt', 'w') as f:
...     f.undefined_function()
... 
Exception has been handled
>>> 

修改后的方法返回True, 表示这个异常已经被(以某种方法)处理了,with也不会再抛出异常

推荐阅读