Python 编程/上下文管理器
编程中的一个基本问题是资源管理:资源是指任何有限供应的东西,特别是文件句柄、网络套接字、锁等,一个关键问题是确保这些资源在获取后被释放。如果它们没有被释放,你将遇到资源泄漏,系统可能会变慢或崩溃。更一般地说,除了释放资源之外,你可能希望始终执行清理操作。
Python 在with
语句中提供了用于此目的的特殊语法,该语句会自动管理封装在上下文管理器类型中的资源,或更一般地执行围绕代码块的启动和清理操作。你应该始终使用 with
语句进行资源管理。许多内置的上下文管理器类型,包括 File
的基本示例,并且编写自己的上下文管理器类型很容易。代码并不难,但概念有点微妙,很容易出错。
基本资源管理使用显式的 open()...close()
函数对,就像在基本的文件打开和关闭中一样。不要这样做,因为我们会解释原因
f = open(filename)
# ...
f.close()
这段简单代码的关键问题是,如果存在早期返回,无论是因为 return
语句还是异常(可能由被调用代码引发),代码都会失败。为了解决这个问题,确保在退出代码块时调用清理代码,可以使用 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 Statement Context Managers)。
对于具有 close
方法但没有 __exit__()
的较旧文件式对象,可以使用 @contextlib.closing
装饰器。如果你需要自己编写,这非常容易,特别是使用 @contextlib.contextmanager
装饰器。[1]
上下文管理器通过在进入 with
上下文时调用 __enter__()
来工作,并将返回值绑定到 as
的目标,并在退出上下文时调用 __exit__()
。在退出期间处理异常有一些微妙之处,但对于简单使用,你可以忽略它。
更微妙的是,__init__()
在创建对象时被调用,而 __enter__()
在进入 with
上下文时被调用。
__init__()
/__enter__()
的区别对于区分一次性、可重用和可重入的上下文管理器非常重要。对于在 with
子句中实例化对象的常见用例,它不是一个有意义的区别,如下所示
with A() as a:
...
...在这种情况下,任何一次性上下文管理器都可以。
但是,总的来说,这是一个区别,特别是在区分可重用的上下文管理器和它所管理的资源时,如这里
a_cm = A()
with a_cm as a:
...
将资源获取放在 __enter__()
而不是 __init__()
中将提供一个可重用的上下文管理器。
值得注意的是,File()
对象在 __init__()
中执行初始化,然后在进入上下文时只返回自身,如 def __enter__(): return self
。如果你希望 as
的目标绑定到一个对象(并允许你使用像 open
这样的工厂作为 with
子句的来源),这样做是可以的,但如果你希望它绑定到其他东西,特别是句柄(文件名或文件句柄/文件描述符),你希望将实际对象包装在一个单独的上下文管理器中。例如
@contextmanager
def FileName(*args, **kwargs):
with File(*args, **kwargs) as f:
yield f.name
对于简单使用,你不需要做任何 __init__()
代码,只需要将 __enter__()
/__exit__()
配对即可。对于更复杂的使用,你可以拥有可重入的上下文管理器,但这对于简单使用来说是不必要的。
请注意,try...finally
子句是必要的,因为 @contextlib.contextmanager
不会捕获 yield
之后引发的任何异常,但在 __exit__()
中不是必要的,因为即使引发异常,也会调用 __exit__()
。
上下文管理器的术语是经过精心选择的,特别是在与“作用域”形成对比时。Python 中的局部变量具有函数作用域,因此 with
语句的目标(如果有的话)在代码块退出后仍然可见,尽管 __exit__()
已经对上下文管理器(with
语句的参数)调用,因此通常没有用或无效。这是一个技术点,但值得区分 with
语句上下文与整个函数作用域。
持有或使用资源的生成器有点棘手。
请注意,在 with
语句中创建生成器并在代码块之外使用它们不会起作用,因为生成器具有延迟计算,因此当它们被计算时,资源已经被释放。这在使用文件时最容易看到,例如以下生成器表达式用于将文件转换为行列表,并剥离行尾字符
with open(filename) as f:
lines = (line.rstrip('\n') for line in f)
当 lines
随后被使用时,可以强制计算 list(lines)
,这将以ValueError: I/O operation on closed file 失败。这是因为文件在 with
语句结束时被关闭,但行直到生成器被计算时才会被读取。
最简单的解决方案是避免生成器,而是使用列表,例如列表推导。在这种情况(读取文件)下,这通常是合适的,因为人们希望最大限度地减少系统调用,并且一次性读取整个文件(除非文件非常大)
with open(filename) as f:
lines = [line.rstrip('\n') for line in f]
如果希望在生成器中使用资源,则必须将资源保存在生成器中,如以下生成器函数所示
def stripped_lines(filename):
with open(filename) as f:
for line in f:
yield line.rstrip('\n')
嵌套清楚地表明,文件在遍历它时一直保持打开状态。
要释放资源,必须使用 generator.close(),
显式关闭生成器,就像处理其他持有资源的对象一样(这是一种处置模式)。这可以通过将生成器变成上下文管理器来实现自动化,使用 @contextlib.closing,
如下所示
from contextlib import closing
with closing(stripped_lines(filename)) as lines:
# ...
资源获取即初始化是一种资源管理的替代形式,特别是在 C++ 中使用。在 RAII 中,资源是在对象构造期间获取的,并在对象析构期间释放。在 Python 中,类似的函数是 __init__()
和 __del__()
(终结器),但 RAII不适用于 Python,并且在 __del__()
中释放资源不起作用。这是因为不能保证 __del__()
会被调用:它只是为了内存管理器使用,而不是为了资源处理。
更详细地讲,Python 对象构造分两阶段:在 `__new__()` 中进行内存分配,在 `__init__()` 中进行属性初始化。 Python 使用引用计数进行垃圾回收,对象通过 `__del__()` 完成最终化(而不是销毁)。 然而,最终化是非确定性的(对象的生命周期是非确定性的),最终化器可能被调用得更晚,甚至根本不调用,尤其是在程序崩溃的情况下。 因此,使用 `__del__()` 进行资源管理通常会导致资源泄漏。
可以使用最终化器进行资源管理,但由此产生的代码依赖于实现(通常在 CPython 中有效,但在其他实现中无效,例如 PyPy),并且易受版本变更的影响。 即使进行了此操作,也需要非常小心地确保在所有情况下引用都降为零,包括:异常,如果捕获异常或以交互方式运行,则异常会在跟踪中包含引用;以及全局变量中的引用,这些引用会在程序终止之前一直存在。 在 Python 3.4 之前,循环中对象的最终化器也是一个严重的问题,但现在不再是问题了;但是,循环中对象的最终化顺序并非确定性的。
- ↑ Nils von Barth 对“如何删除由 Python tempfile.mkdtemp 创建的目录”的回答,StackOverflow
- 将程序作为上下文管理器
- PyMOTW(每周模块):contextlib
- Markus Gattol:上下文管理器