PEP 325 – 生成器资源释放支持
- 作者:
- Samuele Pedroni <pedronis at python.org>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建:
- 2003-08-25
- Python 版本:
- 2.4
- 历史记录:
摘要
生成器允许对数据遍历进行自然编码和抽象。目前,如果涉及需要及时释放的外部资源,则生成器是不够的。及时释放的典型习惯用法不受支持,在生成器内的 try-finally 语句的 try 子句中不允许使用 yield 语句。无法保证或强制执行 finally 子句的执行。
此 PEP 提出内置生成器类型实现一个 close 方法和销毁语义,以便可以取消对 yield 位置的限制,扩展生成器的适用性。
宣告
拒绝,原因是 PEP 342,该提案以更精炼的形式包含了几乎所有请求的行为。
基本原理
Python 生成器允许对许多数据遍历场景进行自然编码。它们的实例化会生成迭代器,即抽象遍历的一流对象(具有所有一流的优点)。在这方面,它们在功能上与使用带有一个(类似 Smalltalk)块的迭代器方法的方法相匹配,并且提供了一些优势。另一方面,鉴于当前的限制(在生成器内部的 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,因为 try-finally 语义要求无法保证 finally 子句的执行。
建议的 close 方法的语义应该是,虽然仍然无法保证 finally 子句的执行,但可以在需要时强制执行。具体来说,close 方法的行为应该触发生成器内 finally 子句的执行,要么通过在生成器框架中强制返回,要么通过在其中抛出异常。在需要及时释放资源的情况下,可以显式调用 close。
另一方面,应该扩展生成器销毁的语义,以便为一般情况实施最佳努力策略。具体来说,销毁应该调用 close()
。最佳努力限制来自这样一个事实,即首先不保证析构函数的执行。
这似乎是一个合理的折衷方案,因为最终的全局行为类似于文件和关闭。
可能的语义
内置生成器类型应该有一个实现的 close 方法,该方法可以像
gen.close()
一样调用,其中 gen
是内置生成器类型的实例。生成器销毁也应该调用 close 方法的行为。
如果生成器已终止,则 close 应该是一个空操作。
否则,有两个替代解决方案,返回或异常语义
A - 返回语义:应该恢复生成器,生成器执行应该继续,就好像重新进入点的指令是返回一样。因此,在重新进入点周围的 finally 子句将被执行,对于 then 允许的 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