在编程中,资源管理是一个基本问题:资源是有限供应的任何东西,尤其是文件句柄、网络套接字、锁等,一个关键问题是确保资源在被获取后被释放。如果资源没有被释放,就会出现资源泄漏,系统可能会变慢或崩溃。更一般的来说,你可能还希望在某些情况下始终执行清理操作,而不仅仅是释放资源。

Python 提供了用于此目的的特殊语法——with 语句,它自动管理被上下文管理器型封装的资源,或者更一般地,在一段代码块周围执行启动和清理操作。你应该始终使用 with 语句来管理资源。Python 提供了许多内置的上下文管理器型,包括基本的文件管理例子,编写自己的上下文管理器也很容易。代码并不复杂,但概念稍微有些微妙,容易出错。

基本资源管理

基本资源管理使用显式的 open()...close() 函数,例如基本的文件打开和关闭。出于以下原因,不要这样做:

f = open(filename)
# ...
f.close()

这个简单代码的关键问题是,如果发生了早期返回,无论是由于返回语句还是由调用的代码抛出的异常,都会导致 close() 不被调用。为了解决这个问题,确保在退出块时调用清理代码,可以使用 try...finally 语句:

f = open(filename)
try:
    # ...
finally:
    f.close()

然而,这仍然需要手动释放资源,并且释放代码与获取资源的代码分离。通过使用 with 语句,可以自动释放资源,这对于上下文管理器型(如 File)非常有效:

with open(filename) as f:
    # ...

这将 open(filename) 的返回值赋给变量 f(这个细节在上下文管理器之间有所不同),并且在块退出时自动释放资源,这里就是调用 f.close()

技术细节

较新的对象是上下文管理器(正式的上下文管理器型:子型,它们实现了上下文管理器接口,包括 __enter__()__exit__()),因此可以很容易地用于 with 语句(详见 with 语句上下文管理器)。

对于旧的文件对象,它们有一个 close() 方法,但没有 __exit__() 方法,可以使用 @contextlib.closing 装饰器。如果你需要自定义上下文管理器,使用 @contextlib.contextmanager 装饰器非常简单。

上下文管理器通过在进入 with 块时调用 __enter__(),将返回值绑定到 as 后面的目标,并在退出 with 块时调用 __exit__()。关于异常处理的细节可以稍微忽略,除非用于更复杂的场景。

更细微地说,__init__() 在对象创建时调用,但 __enter__()with 块进入时调用。

__init__()__enter__() 的区别对于区分单次使用、可重用和可重入的上下文管理器非常重要。例如,在下面的代码中:

a_cm = A()
with a_cm as a:
    ...

这里的 A() 实例化的上下文管理器适用于一次性使用,但如果你将资源的获取放在 __enter__() 而不是 __init__() 中,这样就能得到一个可重用的上下文管理器。

注意事项

  • try...finally:注意,@contextlib.contextmanager 需要 try...finally 语句,因为它不会捕获 yield 后抛出的异常,而 __exit__() 即使异常发生也会被调用。
  • 上下文,而不是作用域:Python 中的局部变量具有函数作用域,因此 with 语句中的目标,如果有的话,在退出块后仍然可见,但 __exit__() 已经调用。因此,要区分 with 语句上下文和整个函数作用域

生成器

持有或使用资源的生成器有点棘手。

需要注意的是,在 with 语句中创建生成器并在块外使用它们时可能会失败,因为生成器是延迟求值的,因此当它们被求值时,资源已经被释放。最简单的解决方法是避免使用生成器,改用列表,例如使用列表推导式。

如果确实需要在生成器中使用资源,资源必须保留在生成器内,如以下生成器函数所示:

def stripped_lines(filename):
    with open(filename) as f:
        for line in f:
            yield line.rstrip('\n')

RAII(资源获取即初始化)

资源获取即初始化(RAII)是 C++ 中常用的资源管理方法。在 RAII 中,资源在对象构造时获取,并在对象销毁时释放。在 Python 中,似的功能是通过 __init__()__del__()(析构函数)来实现的,但 RAII 在 Python 中并不起作用,因为没有保证 __del__() 会被调用,资源管理时使用 __del__() 会导致资源泄漏。

参考资料

  • Nils von Barth’s answer to “how to delete dir created by python tempfile.mkdtemp”,StackOverflow
  • Context Manager Types
  • With Statement Context Managers
Last modified: Friday, 31 January 2025, 1:04 AM