Python 中的上下文管理器

By | 12 12 月, 2018

Python 的上下文管理器可以帮助我们控制和管理各种各样的系统资源(比如文件、锁和连接等),再加上 with 语法糖的使用,可以大大简化我们管理资源的代码成本,同时提高健壮性。
一些常用的场景如下:

with open('testfile', 'w') as f:
    f.write('hello')
import threading
lock = threading.Lock()
with lock:
    print('Lock')

with obj 语句的执行流程如下:

  • 执行 obj.__enter__() 来说明当前正在进入一个新的上下文
  • obj.__enter__() 会返回一个对象用于 with obj as ... 后的变量,当然也可以为空,这是可选的,通常情况下会返回 self自身
  • 执行 with 语句块中的语句
  • 当离开上下文后,执行 obj.__exit__(type, value, traceback) 方法
    • 如果没有任何异常产生,那么三个参数都是 None
    • 如果存在异常,那么就会设置对应的参数
  • obj.__exit__ 方法会返回 True 或 False,分别表示被引发的异常是否得到了处理,如果为 False,那么异常会继续向上抛

下面是一个例子:

class ListTransaction(object):
    def __init__(self, thelist):
        self.thelist = thelist
    def __enter__(self):
        self.workingcopy = list(self.thelist)
        return self.workingcopy
    def __exit__(self, exctype, value, tb):
        if exctype is None:
            self.thelist[:] = self.workingcopy
        return False
items = [1, 2, 3]
with ListTransaction(items) as working:
    working.append(4)
    working.append(5)
print(items)
try:
    with ListTransaction(items) as working:
        working.append(6)
        working.append(7)
        raise RuntimeError
except RuntimeError:
    pass
print(items)

输出如下:

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]

可以看到,对已有列表的一系列修改只会在没有异常发生的时候才会生效,并使得原始列表变化。
另外标准库中提供了一个 contextmanager 的包装生成器函数,用来更加方便的自定义上下文管理器,比如下面:

from contextlib import contextmanager
@contextmanager
def ListTransaction(thelist):
    workingcopy = list(thelist)
    try:
        yield workingcopy
        thelist[:] = workingcopy
    except RuntimeError:
        print('nothing changed')
items = [1, 2, 3]
with ListTransaction(items) as working:
    working.append(4)
    working.append(5)
print(items)
with ListTransaction(items) as working:
    working.append(6)
    working.append(7)
    raise RuntimeError
print(items)

输出如下:

[1, 2, 3, 4, 5]
nothing changed
[1, 2, 3, 4, 5]

yield 语句是一个分界线:

  • yield 之前的语句可以看做是 __enter__() 方法
  • 传递给 yield 的值用作了 __enter__() 方法的返回值
  • 调用 __exit__() 方法时,执行将在 yield 语句后恢复,如果发生了异常,yield 后面的语句将不会执行,当然你可以选择 try catch 住或者直接不管向上抛

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注