PEP 325 – 生成器的资源释放支持
- 作者:
- Samuele Pedroni <pedronis at python.org>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2003年8月25日
- Python 版本:
- 2.4
- 发布历史:
摘要
生成器允许对数据遍历进行自然的编码和抽象。目前,如果涉及需要及时正确释放的外部资源,生成器不幸地不够适用。典型的及时释放惯用法不受支持,在生成器内部的 try-finally 语句的 try 子句中不允许使用 yield 语句。finally 子句的执行既不能保证也不能强制。
本 PEP 提议内置的生成器类型实现一个 close 方法和销毁语义,以便解除对 yield 放置的限制,从而扩展生成器的适用性。
声明
被否决,转而支持 PEP 342,该 PEP 以更精炼的形式包含了所有请求的行为。
基本原理
Python 生成器允许对许多数据遍历场景进行自然编码。它们的实例化会产生迭代器,即抽象遍历的第一类对象(具有第一类对象的所有优点)。在这方面,它们在功能上与使用接受(小talk式)块的迭代器方法的方法相匹配,并提供了一些优势。另一方面,鉴于目前的限制(在生成器内部的 try-finally 语句的 try 子句中不允许使用 yield),后一种方法似乎更适合封装遍历以及异常处理和适当的资源获取和释放。
我们来看一个例子(为简单起见,使用只读模式的文件)
def all_lines(index_path):
for path in file(index_path, "r"):
for line in file(path.strip(), "r"):
yield line
这很简短扼要,但无法添加 try-finally 来及时关闭文件。(虽然可以传入文件而不是路径作为参数,文件的关闭将由调用者负责,但对于根据索引内容打开的文件,这不适用)。
如果我们想要及时释放,我们必须牺牲纯生成器方法的简洁性和直接性:(例如)
class AllLines:
def __init__(self, index_path):
self.index_path = index_path
self.index = None
self.document = None
def __iter__(self):
self.index = file(self.index_path, "r")
for path in self.index:
self.document = file(path.strip(), "r")
for line in self.document:
yield line
self.document.close()
self.document = None
def close(self):
if self.index:
self.index.close()
if self.document:
self.document.close()
使用方式如下
all_lines = AllLines("index.txt")
try:
for line in all_lines:
...
finally:
all_lines.close()
这种实现及时释放的更复杂的解决方案似乎提供了一个宝贵的提示。我们所做的就是将遍历封装在一个带有 close 方法的对象(迭代器)中。
本 PEP 提议生成器应该添加这样一个具有以下语义的 close 方法,以便可以将示例改写为
# Today this is not valid Python: yield is not allowed between
# try and finally, and generator type instances support no
# close method.
def all_lines(index_path):
index = file(index_path, "r")
try:
for path in index:
document = file(path.strip(), "r")
try:
for line in document:
yield line
finally:
document.close()
finally:
index.close()
all = all_lines("index.txt")
try:
for line in all:
...
finally:
all.close() # close on generator
目前 PEP 255 不允许在 try-finally 语句的 try 子句中包含 yield,因为 finally 子句的执行不能像 try-finally 语义所要求的那样得到保证。
所提议的 close 方法的语义应该使得虽然 finally 子句的执行仍然不能保证,但在需要时可以强制执行。具体来说,close 方法的行为应该触发生成器内部 finally 子句的执行,无论是通过强制生成器帧中的返回还是通过在其中抛出异常。在需要及时释放资源的情况下,可以显式调用 close。
另一方面,生成器销毁的语义应该扩展,以便为一般情况实现最佳努力策略。具体来说,销毁应该调用 close()。最佳努力限制来自于析构函数的执行本身不被保证这一事实。
这似乎是一个合理的折衷,由此产生的全局行为与文件及其关闭行为相似。
可能的语义
内置的生成器类型应该实现一个 close 方法,然后可以按如下方式调用
gen.close()
其中 gen 是内置生成器类型的一个实例。生成器销毁也应该调用 close 方法的行为。
如果生成器已经终止,close 应该是一个空操作。
否则,有两种替代解决方案:返回语义或异常语义
A - 返回语义:生成器应该恢复,生成器执行应该继续,就好像重新进入点的指令是一个返回。因此,围绕重新进入点的 finally 子句将被执行,在允许 try-yield-finally 模式的情况下。
问题:区分由 close 强制终止、正常终止、从生成器或生成器调用的代码传播的异常是否重要?在正常情况下似乎不重要,finally 子句应该在所有这些情况下以相同的方式工作,但这种语义可能会使这种区分变得困难。
Except 子句,就像正常返回一样,不执行,遗留生成器中的此类子句期望在由生成器或从其调用的代码引发异常时执行。在 close 情况下不执行它们似乎是正确的。
B - 异常语义:生成器应该恢复,执行应该继续,就好像在重新进入点抛出了一个特殊用途的异常(例如 CloseGenerator)。Close 实现应该消费并不再传播此异常。
问题:是否应该为此目的重用 StopIteration?可能不应该。我们希望 close 对遗留生成器是无害的操作,这些生成器可能包含捕获 StopIteration 以处理其他生成器/迭代器的代码。
通常,使用异常语义,如果生成器不终止或者我们没有收到传播回的特殊异常,则不清楚该怎么做。其他不同的异常可能应该传播,但请考虑以下可能的遗留生成器代码
try:
...
yield ...
...
except: # or except Exception:, etc
raise Exception("boom")
如果在 yield 之后生成器暂停时调用 close,则 except 子句将捕获我们的特殊目的异常,因此我们将收到一个不同的异常传播回来,在这种情况下应该合理地消费和忽略,但通常应该传播,但分离这些场景似乎很困难。
异常方法让生成器能够区分终止情况并拥有更多控制权,这具有优势。另一方面,清晰的语义似乎更难定义。
备注
如果此提案被接受,那么记录生成器是否获取资源(以便调用其 close 方法)应成为常见做法。如果生成器不再使用,调用 close 应该是无害的。
另一方面,在典型场景中,实例化生成器的代码应在需要时调用 close。处理在其他地方实例化的迭代器/生成器的通用代码通常不应充斥着 close 调用。
对于那些已经获取了所有迭代器、生成器以及需要及时释放资源的生成器的所有权并需要正确处理的罕见情况,可以很容易地解决
if hasattr(iterator, 'close'):
iterator.close()
未解决的问题
必须选择明确的语义。目前 Guido 倾向于异常语义。如果生成器生成一个值而不是终止,或者传播回特殊异常,则应在生成器端再次引发特殊异常。
目前还不清楚是否有虚假转换的特殊异常(如可能的语义中所讨论的)是一个问题,以及如何处理它们。
应该探讨实现问题。
替代方案
关于应移除 yield 放置限制以及生成器销毁应触发 finally 子句执行的想法已被多次提出。单独来看,它不能保证可以强制及时释放生成器获取的资源。
PEP 288 提出了一种更通用的解决方案,允许将自定义异常传递给生成器。本 PEP 中的提议更直接地解决了资源释放问题。如果 PEP 288 得到实现,那么 close 的异常语义可以在其之上分层,另一方面 PEP 288 应该为更通用的功能单独提出一个案例。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0325.rst