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