14.2 上下文管理器

Python 2.6中引入的with语句,可能会让过去的Lisp程序员想起以前经常用到的宏with-*。Python通过使用实现了上下文管理协议的对象,提供了类似的机制。

open函数返回的对象就支持这个协议,这就是经常能看到下面这样的代码的原因:

广告:个人专属 VPN,独立 IP,流量大,速度快,连接稳定,多机房切换,每月最低仅 10 美元

with open("myfile", "r") as f:
  line = f.readline()

open返回的对象有两个方法,一个称为__enter__,另一个称为__exit__。它们分别在with块开始和结束时被调用。

一个上下文对象的简单实现如示例14.1所示。

示例 14.1  上下文对象的简单实现

class MyContext(object):
  def __enter__(self):
    pass
  def __exit__(self, exc_type, exc_value, traceback):
    pass

这段代码什么都不做,但却是合法的。

你想什么时候使用上下文管理器呢?如果对象符合下面的模式,则使用上下文管理协议就比较合适:

(1)调用方法A;

(2)执行一段代码;

(3)调用方法B。

这里希望调用方法B必须总是调用方法A之后。open函数很好地阐明了这一模式,打开文件并在内部分配一个文件描述符的构造函数便是方法A。释放对应文件描述符的close方法就是方法B。显然,close方法总是应该在实例化文件对象之后进行调用。

contextlib标准库中提供了contextmanager,通过生成器构造__enter____exit__方法,从而简化了这一机制的实现。可以使用它实现自己的简单上下文管理器,如示例14.2所示。

示例 14.2 contextlib.contextmanager的简单用法

import contextlib

@contextlib.contextmanager
def MyContext():
  yield

例如,我曾经在Ceilometer(https://launchpad.net/ceilometer)中对我们所建立的流水线(pipeline)架构使用过这种设计模式。简单来说,一个流水线就是一个管道,一方面传入对象,另一方面将对象分发到不同的地方。发送数据的步骤如下。

(1)调用流水线的publish(objects)方法,并传入你的对象作为参数(可以调用任意多次)。

(2)一旦完成,则调用flush()方法以表明当前的发布已经完成。

要注意的是,如果不调用flush()方法,对象将不会被发送到管道中,或者至少不完全发送到管道中。程序员很容易忘记flush()的调用,这将引起程序毫无征兆地中断。

最好能让API提供一个上下文管理器对象,去阻止API的用户犯这种错误。通过示例14.3所示的代码很容易实现。

示例 14.3  在流水线对象上使用上下文管理器

import contextlib

class Pipeline(object):
  def _publish(self, objects):
    # Imagine publication code here
    pass

  def _flush(self):
    # Imagine flushing code here
    pass

  @contextlib.contextmanager
  def publisher(self):
    try:
      yield self._publish
    finally:
      self._flush()

现在,当用户在使用流水线发布某些数据时,他们无需使用_publish或者_flush。用户只需请求一个使用了名祖(eponym)函数的publisher并使用它。

pipeline = Pipeline()
with pipeline.publisher() as publisher:
  publisher([1, 2, 3, 4])

当提供一个这样的API时,就不会遇到用户错误。当看到符合的设计模式时,应该尽量用上下文管理器。

在某些情况下,同时使用多个上下文管理器是很有用的。例如,同时打开两个文件以复制它们的内容,如示例14.4所示。

示例 14.4  同时打开两个文件

with open("file1", "r") as source:
  with open("file2", "w") as destination:
    destination.write(source.read())

记住with语句可以支持多个参数,所以应该像示例14.5这样写。

示例 14.5  通过一条with语句同时打开两个文件

with open("file1", "r") as source, open("file2", "w") as destination:
  destination.write(source.read())