Python上下文管理器

Python上下文管理器

上下文管理器

有时候我们需要在当程序在语句块中运行时保持某种状态,并且在离开语句块后结束这种状态。所以,事实上上下文管理器的任务是 – 代码块执行前准备,代码块执行后收拾。
上下文管理器允许你在有需要的时候,精确地分配和释放资源。使用上下文管理器最广泛的案例就是with语句了。
上下文管理器的一个常见用例,是资源的加锁和解锁,以及关闭已打开的文件。

基于类的实现

一个上下文管理器的类,最起码要定义enterexit方法。

1
2
3
4
5
6
7
8
9
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
self.file_obj.close()

我们的exit函数接受三个参数。这些参数对于每个上下文管理器类中的exit方法都是必须的。我们来谈谈在底层都发生了什么。

  • 1.with语句先暂存了File类的exit方法
  • 2.然后它调用File类的enter方法
  • 3.enter方法打开文件并返回给with语句
  • 4.打开的文件句柄被传递给opened_file参数
  • 5.我们使用.write()来写文件
  • 6.with语句调用之前暂存的exit方法
  • 7.exit方法关闭了文件

处理异常

我们还没有谈到exit方法的这三个参数:type, value和traceback。
在第4步和第6步之间,如果发生异常,Python会将异常的type,value和traceback传递给exit方法。
它让exit方法来决定如何关闭文件以及是否需要其他步骤。
我们来列一下,当异常发生时,with语句会采取哪些步骤。

  • 1.它把异常的type,value和traceback传递给exit方法
  • 2.它让exit方法来处理异常
  • 3.如果exit返回的是True,那么这个异常就被优雅地处理了。
  • 4.如果exit返回的是True以外的任何东西,那么这个异常将被with语句抛出。
    在我们的案例中,exit方法返回的是None(如果没有return语句那么方法会返回None)。因此,with语句抛出了那个异常。
    我们的exit方法返回了True,因此没有异常会被with语句抛出。

基于生成器的实现

我们还可以用装饰器(decorators)和生成器(generators)来实现上下文管理器。
Python有个contextlib模块专门用于这个目的。我们可以使用一个生成器函数来实现一个上下文管理器,而不是使用一个类。
让我们看看一个基本的,没用的例子:

1
2
3
4
5
6
7
8
9
10
11
from contextlib import contextmanager
@contextmanager
def open_file(name):
try:
f = open(name, 'w')
yield f
except Exception as e:
print(e)
finally:
f.close()

在这个例子中我们还没有捕捉可能产生的任何异常。它的工作方式和之前的方法大致相同。
让我们小小地剖析下这个方法。

  • 1.Python解释器遇到了yield关键字。因为这个缘故它创建了一个生成器而不是一个普通的函数。
  • 2.因为这个装饰器,contextmanager会被调用并传入函数名(open_file)作为参数。
  • 3.contextmanager函数返回一个以GeneratorContextManager对象封装过的生成器。
  • 4.这个GeneratorContextManager被赋值给open_file函数,我们实际上是在调用GeneratorContextManager对象。

那现在我们既然知道了所有这些,我们可以用这个新生成的上下文管理器了,像这样:

1
2
with open_file('some_file') as f:
f.write('hola!')

如果你希望在上下文管理器中使用“as”关键字,那么就用yield返回你需要的值,它将通过as关键字赋值给新的变量。

谈一些关于上下文库(contextlib)的内容

contextlib是一个Python模块,作用是提供更易用的上下文管理器。
contextlib.closing
假设我们有一个创建数据库函数,它将返回一个数据库对象,并且在使用完之后关闭相关资源(数据库连接会话等)
我们可以像以往那样处理或是通过上下文管理器:

1
2
with contextlib.closing(CreateDatabase()) as database:
database.query()

contextlib.closing方法将在语句块结束后调用数据库的关闭方法。

contextlib.nested
假设我们有两个文件,一个读一个写,需要进行拷贝。
以下是不提倡的:

1
2
3
with open('toReadFile', 'r') as reader:
with open('toWriteFile', 'w') as writer:
writer.writer(reader.read())

可以通过contextlib.nested进行简化:

1
2
with contextlib.nested(open('fileToRead.txt', 'r'), open('fileToWrite.txt', 'w')) as (reader, writer):
writer.write(reader.read())

在Python2.7中这种写法被一种新语法取代:

1
2
with open('fileToRead.txt', 'r') as reader, open('fileToWrite.txt', 'w') as writer:
writer.write(reader.read())

contextlib.contextmanager
对于Python高级玩家来说,任何能够被yield关键词分割成两部分的函数,都能够通过装饰器装饰的上下文管理器来实现。任何在yield之前的内容都可以看
做在代码块执行前的操作,而任何yield之后的操作都可以放在exit函数中。
如果你希望在上下文管理器中使用“as”关键字,那么就用yield返回你需要的值,它将通过as关键字赋值给新的变量。
contextlib库中除了上述的几个常见的上下文管理器函数以外,还有contextlib.suppress(*exceptions)、contextlib.redirect_stdout / redirect_stderr、contextlib.ContextDecorator、contextlib.ExitStack等函数可以适用于不同的情景。

文章目录
  1. 1. 上下文管理器
  2. 2. 基于类的实现
  3. 3. 处理异常
  4. 4. 基于生成器的实现
  5. 5. 谈一些关于上下文库(contextlib)的内容
|