Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 533 – 迭代器的确定性清理

作者:
纳撒尼尔·J·史密斯
BDFL 委托
Yury Selivanov <yury at edgedb.com>
状态:
推迟
类型:
标准跟踪
创建日期:
2016年10月18日
发布历史:
2016年10月18日

目录

摘要

我们建议扩展迭代器协议,添加一个新的 __(a)iterclose__ 槽位,该槽位在退出 (async) for 循环时自动调用,无论退出方式如何。这使得迭代器持有的资源可以方便地、确定性地清理,而无需依赖垃圾回收器。这对于异步生成器尤其有价值。

关于时机的说明

实际上,这里的提案分为两个独立的部分:异步迭代器的处理,这应该尽快实现;以及常规迭代器的处理,这是一个更大但更宽松的项目,最早要到 3.7 版本才能开始。但由于这些更改密切相关,而且我们可能不希望异步迭代器和常规迭代器长期分歧,因此将它们放在一起考虑似乎很有用。

背景与动机

Python 可迭代对象通常持有需要清理的资源。例如:file 对象需要关闭;WSGI 规范在常规迭代器协议之上添加了一个 close 方法,并要求消费者在适当的时候调用它(尽管忘记调用是一个常见的错误来源);以及PEP 342(基于PEP 325)扩展了生成器对象,添加了一个 close 方法,允许生成器自行清理。

通常,需要自行清理的对象也会定义一个 __del__ 方法,以确保在对象被垃圾回收时最终会发生此清理。然而,在某些情况下,依赖垃圾回收器进行此类清理会造成严重问题。

  • 在不使用引用计数(例如 PyPy、Jython)的 Python 实现中,对 __del__ 的调用可能会被任意延迟——然而许多情况需要对资源进行*即时*清理。延迟清理会导致文件描述符耗尽导致的崩溃,或者 WSGI 计时中间件收集到错误时间等问题。
  • 异步生成器(PEP 525)只能在适当的协程运行器监督下执行清理。__del__ 无法访问协程运行器;实际上,协程运行器可能会在生成器对象之前被垃圾回收。因此,不使用某种语言扩展,依赖垃圾回收器几乎是不可能的。(PEP 525 确实提供了这样的扩展,但它存在许多限制,本提案修复了这些限制;请参阅下面的“替代方案”部分进行讨论。)

幸运的是,Python 提供了一个标准工具,以更结构化的方式进行资源清理:with 块。例如,这段代码打开一个文件,但依赖垃圾回收器来关闭它

def read_newline_separated_json(path):
    for line in open(path):
        yield json.loads(line)

for document in read_newline_separated_json(path):
    ...

最近的 CPython 版本会通过发出 ResourceWarning 来指出这一点,促使我们通过添加一个 with 块来修复它

def read_newline_separated_json(path):
    with open(path) as file_handle:      # <-- with block
        for line in file_handle:
            yield json.loads(line)

for document in read_newline_separated_json(path):  # <-- outer for loop
    ...

但这里存在一个微妙之处,这是由 with 块和生成器之间的相互作用引起的。with 块是 Python 用于管理清理的主要工具,它是一个强大的工具,因为它将资源的生命周期绑定到堆栈帧的生命周期。但这假设有人会负责清理堆栈帧……而对于生成器,这需要有人 close 它们。

在这种情况下,添加 with 块*确实*足以消除 ResourceWarning,但这具有误导性——这里的文件对象清理仍然依赖于垃圾回收器。with 块只会在 read_newline_separated_json 生成器关闭时才会被解除。如果外部 for 循环运行完成,那么清理会立即发生;但如果此循环因 break 或异常而提前终止,那么 with 块在生成器对象被垃圾回收之前不会触发。

正确的解决方案要求此 API 的所有*用户*将其每个 for 循环包装在自己的 with 块中

with closing(read_newline_separated_json(path)) as genobj:
    for document in genobj:
        ...

如果我们考虑将复杂管道分解为多个嵌套生成器的惯用语,情况会变得更糟

def read_users(path):
    with closing(read_newline_separated_json(path)) as gen:
        for document in gen:
            yield User.from_json(document)

def users_in_group(path, group):
    with closing(read_users(path)) as gen:
        for user in gen:
            if user.group == group:
                yield user

通常,如果您有 N 个嵌套生成器,那么您需要 N+1 个 with 块来清理 1 个文件。良好的防御性编程会建议,无论何时使用生成器,我们都应该假设其(可能传递的)调用堆栈中现在或将来可能至少存在一个 with 块,因此始终将其包装在 with 中。但实际上,基本上没有人这样做,因为程序员宁愿编写有错误的代码,也不愿编写繁琐重复的代码。在这样的简单情况下,有一些好的 Python 开发人员知道的变通方法(例如,在这个简单情况下,惯用的做法是传入文件句柄而不是路径,并将资源管理移至顶层),但通常我们无法避免在生成器内部使用 with/finally,因此需要以某种方式解决这个问题。当美观和正确性发生冲突时,美观往往会获胜,因此使正确的代码变得美观非常重要。

不过,这值得修复吗?在异步生成器出现之前,我可能会说值得,但优先级较低,因为大家似乎都能凑合着用——但异步生成器使其变得更加紧迫。异步生成器根本无法进行清理,除非有某种人们实际会使用的确定性清理机制,而且异步生成器特别容易持有文件描述符等资源。(毕竟,如果它们不进行 I/O,它们就是生成器,而不是异步生成器。)所以我们必须做些什么,而且最好是对根本问题进行全面修复。现在异步生成器刚刚推出,修复起来比以后容易得多。

提案本身概念简单:在迭代器协议中添加一个 __(a)iterclose__ 方法,并让(异步)for 循环在循环退出时调用它,即使是通过 break 或异常展开退出。实际上,我们正在将当前繁琐的惯用语(with 块 + for 循环)合并成一个更高级的 for 循环。这可能看起来不那么正交,但考虑到生成器的存在意味着 with 块实际上依赖于迭代器清理才能可靠工作,再加上经验表明迭代器清理本身通常是一个理想的特性,这就有道理了。

备选方案

PEP 525 异步生成器钩子

PEP 525 提议了一组由新的 sys.{get/set}_asyncgen_hooks() 函数管理的全局线程局部钩子,允许事件循环与垃圾回收器集成,以运行异步生成器的清理。原则上,本提案和 PEP 525 是互补的,就像 with 块和 __del__ 是互补的一样:本提案负责确保在大多数情况下的确定性清理,而 PEP 525 的 GC 钩子清理任何遗漏的部分。但 __aiterclose__ 相比单独的 GC 钩子有许多优点

  • GC 钩子语义不属于抽象异步迭代器协议的一部分,而是专门限制在异步生成器具体类型上。如果你有一个使用类实现的异步迭代器,像这样
    class MyAsyncIterator:
        async def __anext__():
            ...
    

    那么你无法在不改变其语义的情况下将其重构为异步生成器,反之亦然。这看起来非常不符合 Python 的风格。(它还留下了这样一个问题:鉴于基于类的异步迭代器面临与异步生成器完全相同的清理问题,它们到底应该怎么做。)另一方面,__aiterclose__ 是在协议级别定义的,因此它对鸭子类型友好,并且适用于所有迭代器,而不仅仅是生成器。

  • 希望在非 CPython 实现(如 PyPy)上运行的代码通常不能依赖 GC 进行清理。如果没有 __aiterclose__,几乎可以肯定的是,在 CPython 上开发和测试的开发人员会编写在 PyPy 上使用时会泄漏资源的库。希望针对其他实现的开发人员将不得不采取防御性方法,将每个 for 循环包装在 with 块中,或者仔细审计他们的代码,找出哪些生成器可能包含清理代码,并仅在这些生成器周围添加 with 块。有了 __aiterclose__,编写可移植代码变得简单自然。
  • 构建健壮软件的一个重要部分是确保异常始终正确传播而不会丢失。与传统的基于回调的系统相比,async/await 最令人兴奋的一点是,它不再需要手动链接,运行时现在可以承担传播错误的繁重工作,使得编写健壮代码*容易得多*。但是,这个美好的新景象有一个主要缺陷:如果我们依赖 GC 进行生成器清理,那么清理过程中引发的异常就会丢失。因此,再次强调,有了 __aiterclose__,关注这种健壮性的开发人员将不得不采取防御性方法,将每个 for 循环包装在 with 块中,或者仔细审计他们的代码,找出哪些生成器可能包含清理代码。__aiterclose__ 通过在调用者的上下文中执行清理来弥补这个漏洞,因此编写更健壮的代码成为阻力最小的路径。
  • WSGI 经验表明,存在重要的基于迭代器的 API,它们需要即时清理,不能依赖 GC,即使在 CPython 中也是如此。例如,考虑一个基于 async/await 和异步迭代器的假设 WSGI 类似 API,其中响应处理器是一个异步生成器,它接受请求头 + 请求体的异步迭代器,并产生响应头 + 响应体。(这实际上是我最初对异步生成器感兴趣的用例,即这不是假设的。)如果我们遵循 WSGI 的要求,即子迭代器必须正确关闭,那么如果没有 __aiterclose__,我们系统中最简约的中间件看起来像这样
    async def noop_middleware(handler, request_header, request_body):
        async with aclosing(handler(request_body, request_body)) as aiter:
            async for response_item in aiter:
                yield response_item
    

    可以说在常规代码中,可以跳过 for 循环周围的 with 块,具体取决于对生成器内部实现的理解程度。但在这里我们必须处理任意的响应处理程序,因此如果没有 __aiterclose__,这种 with 构造是每个中间件的强制性部分。

    __aiterclose__ 允许我们从每个中间件中消除强制性的样板代码和额外的缩进级别

    async def noop_middleware(handler, request_header, request_body):
        async for response_item in handler(request_header, request_body):
            yield response_item
    

因此,__aiterclose__ 方法比 GC 钩子提供了显著的优势。

这留下了一个问题:我们是想要 GC 钩子 + __aiterclose__ 的组合,还是仅仅是 __aiterclose__。由于绝大多数生成器都使用 for 循环或类似的方式进行迭代,因此在 GC 有机会介入之前,__aiterclose__ 处理了大多数情况。GC 钩子提供额外价值的情况是执行手动迭代的代码,例如

agen = fetch_newline_separated_json_from_url(...)
while True:
    document = await type(agen).__anext__(agen)
    if document["id"] == needle:
        break
# doesn't do 'await agen.aclose()'

如果采用 GC 钩子 + __aiterclose__ 方法,这个生成器最终将通过 GC 调用生成器的 __del__ 方法进行清理,然后该方法将使用钩子回调到事件循环中运行清理代码。

如果我们采用无 GC 钩子方法,这个生成器最终将被垃圾回收,其效果如下

  • __del__ 方法将发出警告,指出生成器未关闭(类似于现有的“协程从未被等待”警告)。
  • 所涉及的底层资源仍将被清理,因为生成器帧仍将被垃圾回收,导致它放弃对所持有的任何文件句柄或套接字的引用,然后这些对象的 __del__ 方法将释放实际的操作系统资源。
  • 但是,生成器内部的任何清理代码(例如日志记录、缓冲区刷新)将没有机会运行。

这里的解决方案——正如警告所示——是修复代码,使其调用 __aiterclose__,例如通过使用 with

async with aclosing(fetch_newline_separated_json_from_url(...)) as agen:
    while True:
        document = await type(agen).__anext__(agen)
        if document["id"] == needle:
            break

基本上,在这种方法中,规则是如果你想手动实现迭代器协议,那么你有责任实现所有协议,现在这也包括 __(a)iterclose__

GC 钩子以以下形式增加了非平凡的复杂性:(a)新的全局解释器状态,(b)有点复杂的控制流(例如,异步生成器 GC 总是涉及复活,因此 PEP 442 的细节很重要),以及(c)asyncio 中一个新的公共 API(await loop.shutdown_asyncgens()),用户必须记住在适当的时候调用它。(最后一点尤其削弱了 GC 钩子提供安全备份以保证清理的论点,因为如果 shutdown_asyncgens() 没有正确调用,我*认为*生成器可能会被静默丢弃而没有调用其清理代码;将其与仅 __aiterclose__ 方法进行比较,在最坏的情况下我们至少会打印一个警告。这可能是可修复的。)综合考虑,GC 钩子可能不值得,因为它们只帮助那些想要手动调用 __anext__ 但不想手动调用 __aiterclose__ 的人。但 Yury 在这一点上与我意见相左 :-)。两种选择都可行。

始终注入资源,并在顶层执行所有清理

python-dev 和 python-ideas 上的几位评论者建议,避免这些问题的一种模式是始终从上方传入资源,例如 read_newline_separated_json 应该接受文件对象而不是路径,并在顶层处理清理

def read_newline_separated_json(file_handle):
    for line in file_handle:
        yield json.loads(line)

def read_users(file_handle):
    for document in read_newline_separated_json(file_handle):
        yield User.from_json(document)

with open(path) as file_handle:
    for user in read_users(file_handle):
        ...

这在简单情况下效果很好;在这里它让我们避免了“N+1 个 with 块问题”。但不幸的是,当事情变得更复杂时,它很快就会失效。考虑一下,如果我们的生成器不是从文件读取,而是从流式 HTTP GET 请求读取——同时通过 OAUTH 处理重定向和身份验证。那么我们确实希望套接字在我们的 HTTP 客户端库内部进行管理,而不是在顶层。此外,还有其他情况下,嵌入在生成器内部的 finally 块本身也很重要:数据库事务管理,在清理过程中发出日志信息(WSGI close 的主要动机用例之一),等等。所以这实际上是简单情况下的变通方法,而不是通用解决方案。

__(a)iterclose__ 更复杂的变体

__(a)iterclose__ 的语义在某种程度上受到 with 块的启发,但上下文管理器更强大:__(a)exit__ 可以区分正常退出和异常展开,在发生异常时,它可以检查异常细节并选择性地抑制传播。这里提议的 __(a)iterclose__ 没有这些能力,但可以想象一个具有这些能力的替代设计。

然而,这似乎是不必要的复杂性:经验表明,可迭代对象通常具有 close 方法,甚至具有调用 self.close()__exit__ 方法,但我不知道有任何常见情况会利用 __exit__ 的全部功能。我也想不出任何这会很有用的例子。并且允许迭代器通过吞噬异常来影响流控制似乎会不必要地混淆——如果你处于确实需要这种情况的境地,那么你可能仍然应该使用一个真正的 with 块。

规范

本节描述我们最终想要达到的目标,尽管存在一些向后兼容性问题,这意味着我们无法直接跳到这里。后面一节将描述过渡计划。

指导原则

一般来说,__(a)iterclose__ 的实现应该

  • 幂等,
  • 假定在调用 __(a)iterclose__ 后迭代器将不再使用,执行所有适当的清理。特别是,一旦调用了 __(a)iterclose__,那么调用 __(a)next__ 会产生未定义的行为。

通常,任何开始迭代可迭代对象并打算耗尽它的代码,都应该确保最终调用 __(a)iterclose__,无论迭代器是否实际耗尽。

迭代的改变

核心提案是 for 循环行为的改变。给定这段 Python 代码

for VAR in ITERABLE:
    LOOP-BODY
else:
    ELSE-BODY

我们将其分解为等效的

_iter = iter(ITERABLE)
_iterclose = getattr(type(_iter), "__iterclose__", lambda: None)
try:
    traditional-for VAR in _iter:
        LOOP-BODY
    else:
        ELSE-BODY
finally:
    _iterclose(_iter)

这里的“传统 for 语句”是经典 3.5 及更早版本的 for 循环语义的简写。

除了顶层的 for 语句,Python 还包含其他几个消费迭代器的地方。为了保持一致性,这些地方也应该使用与上述等效的语义调用 __iterclose__。这包括

  • 推导式中的 for 循环
  • * 解包
  • 接受并完全消费可迭代对象的函数,例如 list(it)tuple(it)itertools.product(it1, it2, ...) 等。

此外,成功耗尽被调用生成器的 yield from 应该作为最后一步调用其 __iterclose__ 方法。(理由:yield from 已经将调用生成器的生命周期链接到被调用生成器;如果调用生成器在 yield from 中途关闭,那么这将自动关闭被调用生成器。)

异步迭代的改变

我们还对异步迭代构造进行了类似的更改,只是新的槽位被称为 __aiterclose__,它是一个异步方法,会被 await

基本迭代器类型的修改

生成器对象(包括由生成器推导式创建的对象)

  • __iterclose__ 调用 self.close()
  • __del__ 调用 self.close()(与现在相同),如果生成器未耗尽,还会发出 ResourceWarning。此警告默认隐藏,但对于那些希望确保自己不会无意中依赖 CPython 特定的 GC 语义的人可以启用。

异步生成器对象(包括由异步生成器推导式创建的对象)

  • __aiterclose__ 调用 self.aclose()
  • 如果尚未调用 aclose,则 __del__ 会发出 RuntimeWarning,因为这可能表明存在潜在错误,类似于“协程从未被等待”警告。

问题:文件对象是否应该实现 __iterclose__ 来关闭文件?一方面,这会使此更改更具破坏性;另一方面,人们非常喜欢编写 for line in open(...): ...,如果习惯了迭代器自行清理,那么文件不这样做可能会变得非常奇怪。

新的便捷函数

operator 模块增加了两个新函数,其语义等同于以下内容

def iterclose(it):
    if not isinstance(it, collections.abc.Iterator):
        raise TypeError("not an iterator")
    if hasattr(type(it), "__iterclose__"):
        type(it).__iterclose__(it)

async def aiterclose(ait):
    if not isinstance(it, collections.abc.AsyncIterator):
        raise TypeError("not an iterator")
    if hasattr(type(ait), "__aiterclose__"):
        await type(ait).__aiterclose__(ait)

itertools 模块获得了一个新的迭代器包装器,可用于选择性地禁用新的 __iterclose__ 行为

# QUESTION: I feel like there might be a better name for this one?
class preserve(iterable):
    def __init__(self, iterable):
        self._it = iter(iterable)

    def __iter__(self):
        return self

    def __next__(self):
        return next(self._it)

    def __iterclose__(self):
        # Swallow __iterclose__ without passing it on
        pass

示例用法(假设文件对象实现了 __iterclose__

with open(...) as handle:
    # Iterate through the same file twice:
    for line in itertools.preserve(handle):
        ...
    handle.seek(0)
    for line in itertools.preserve(handle):
        ...
@contextlib.contextmanager
def iterclosing(iterable):
    it = iter(iterable)
    try:
        yield preserve(it)
    finally:
        iterclose(it)

迭代器包装器的 __iterclose__ 实现

Python 附带了许多充当其他迭代器包装器的迭代器类型:mapzipitertools.accumulatecsv.reader 等。这些迭代器应该定义一个 __iterclose__ 方法,该方法依次调用其底层迭代器上的 __iterclose__。例如,map 可以实现为

# Helper function
map_chaining_exceptions(fn, items, last_exc=None):
    for item in items:
        try:
            fn(item)
        except BaseException as new_exc:
            if new_exc.__context__ is None:
                new_exc.__context__ = last_exc
            last_exc = new_exc
    if last_exc is not None:
        raise last_exc

class map:
    def __init__(self, fn, *iterables):
        self._fn = fn
        self._iters = [iter(iterable) for iterable in iterables]

    def __iter__(self):
        return self

    def __next__(self):
        return self._fn(*[next(it) for it in self._iters])

    def __iterclose__(self):
        map_chaining_exceptions(operator.iterclose, self._iters)

def chain(*iterables):
    try:
        while iterables:
            for element in iterables.pop(0):
                yield element
    except BaseException as e:
        def iterclose_iterable(iterable):
            operations.iterclose(iter(iterable))
        map_chaining_exceptions(iterclose_iterable, iterables, last_exc=e)

在某些情况下,这需要一些微妙之处;例如,itertools.tee 不应该在所有克隆迭代器都调用过 __iterclose__ 之前,在底层迭代器上调用 __iterclose__

示例/理由

所有这些的益处是,我们现在可以编写像下面这样直接的代码

def read_newline_separated_json(path):
    for line in open(path):
        yield json.loads(line)

并确信文件将获得确定性清理,*而无需最终用户付出任何特殊努力*,即使在复杂情况下也是如此。例如,考虑这个简单的管道

list(map(lambda key: key.upper(),
         doc["key"] for doc in read_newline_separated_json(path)))

如果我们的文件包含一个文档,其中 doc["key"] 结果是一个整数,那么将发生以下事件序列

  1. key.upper() 引发 AttributeError,该异常从 map 传播出来,并触发 list 内部的隐式 finally 块。
  2. list 中的 finally 块调用 map 对象的 __iterclose__()
  3. map.__iterclose__() 调用生成器推导式对象的 __iterclose__()
  4. 这会将一个 GeneratorExit 异常注入到生成器推导式主体中,该主体目前暂停在推导式的 for 循环主体内部。
  5. 异常从 for 循环中传播出来,触发 for 循环的隐式 finally 块,该块在表示对 read_newline_separated_json 调用的生成器对象上调用 __iterclose__
  6. 这会在 read_newline_separated_json 的主体中注入一个内部 GeneratorExit 异常,目前暂停在 yield 处。
  7. 内部 GeneratorExitfor 循环中继续传播,命中生成器函数的边界,并导致 read_newline_separated_json__iterclose__() 方法成功返回。
  8. 文件对象已关闭。
  9. 内部 GeneratorExit 恢复传播,到达生成器函数的边界,并导致 read_newline_separated_json__iterclose__() 方法成功返回。
  10. 控制返回到生成器推导式主体,外部 GeneratorExit 继续传播,允许推导式的 __iterclose__() 成功返回。
  11. 其余的 __iterclose__() 调用顺利展开,返回到 list 的主体。
  12. 原始的 AttributeError 恢复传播。

(上述细节假设我们实现了 file.__iterclose__;如果不是,则向 read_newline_separated_json 添加一个 with 块,并且基本相同的逻辑也会通过。)

当然,从用户的角度来看,这可以简化为

1. int.upper() 引发 AttributeError 1. 文件对象已关闭。 2. AttributeErrorlist 中传播出来

所以我们已经实现了我们的目标,让这一切“just work”,而用户无需考虑。

过渡计划

虽然绝大多数现有的 for 循环将继续产生相同的结果,但所提议的更改在某些情况下将产生向后不兼容的行为。示例

def read_csv_with_header(lines_iterable):
    lines_iterator = iter(lines_iterable)
    for line in lines_iterator:
        column_names = line.strip().split("\t")
        break
    for line in lines_iterator:
        values = line.strip().split("\t")
        record = dict(zip(column_names, values))
        yield record

这段代码以前是正确的,但在实现此提案后,将需要在第一个 for 循环中添加一个 itertools.preserve 调用。

[问题:目前,如果您关闭一个生成器然后尝试迭代它,它只会引发 Stop(Async)Iteration,因此将相同的生成器对象传递给多个 for 循环但忘记使用 itertools.preserve 的代码不会看到明显的错误——第二个 for 循环将立即退出。也许如果迭代一个已关闭的生成器引发 RuntimeError 会更好?请注意,文件没有这个问题——尝试迭代一个已关闭的文件对象已经会引发 ValueError。]

具体来说,不兼容性发生在所有这些因素共同出现时

  • 自动调用 __(a)iterclose__ 已启用
  • 该可迭代对象之前未定义 __(a)iterclose__
  • 该可迭代对象现在定义了 __(a)iterclose__
  • for 循环退出后,该可迭代对象被重新使用

所以问题是如何管理这种过渡,这就是我们必须处理的杠杆。

首先,请注意,我们建议添加 __aiterclose__ 的唯一异步可迭代对象是异步生成器,并且目前没有使用异步生成器的现有代码(尽管这种情况很快会改变),因此异步更改不会产生任何向后不兼容性。(存在使用异步迭代器的现有代码,但对旧异步迭代器使用新的异步 for 循环是无害的,因为旧异步迭代器没有 __aiterclose__。)此外,PEP 525 已在临时基础上接受,异步生成器是本 PEP 提议更改的最大受益者。因此,我认为我们应该强烈考虑尽快为 async for 循环和异步生成器启用 __aiterclose__,理想情况下是在 3.6.0 或 3.6.1 版本。

对于非异步世界,情况更加困难,但这里有一个潜在的过渡路径

在 3.7 版本中

我们的目标是让现有的不安全代码开始发出警告,而那些想要选择未来功能的人可以立即这样做

  • 我们立即添加上述所有 __iterclose__ 方法。
  • 如果 from __future__ import iterclose 生效,那么 for 循环和 * 解包将按照上述规范调用 __iterclose__
  • 如果未来功能*未*启用,那么 for 循环和 * 解包将*不*调用 __iterclose__。但它们会调用其他方法,例如 __iterclose_warning__
  • 同样,像 list 这样的函数使用栈自省 (!!) 来检查它们的直接调用者是否启用了 __future__.iterclose,并以此来决定是调用 __iterclose__ 还是 __iterclose_warning__
  • 对于所有包装器迭代器,我们还添加了 __iterclose_warning__ 方法,这些方法会转发到底层迭代器或迭代器的 __iterclose_warning__ 方法。
  • 对于生成器(以及文件,如果我们决定这样做),__iterclose_warning__ 被定义为设置一个内部标志,并且对象的其他方法被修改以检查此标志。如果它们发现该标志已设置,它们会发出 PendingDeprecationWarning 以通知用户,将来这种序列将导致使用后关闭的情况,并且用户应该使用 preserve()

在 3.8 版本中

  • PendingDeprecationWarning 切换到 DeprecationWarning

在 3.9 版本中

  • 无条件启用 __future__ 并删除所有 __iterclose_warning__ 相关内容。

我相信这满足了这种过渡的正常要求——最初是选择加入,警告精确地针对将受影响的情况,并且有较长的弃用周期。

这其中最具争议性/风险的部分可能是使用堆栈自省使可迭代消费函数对 __future__ 设置敏感,尽管我还没有想到任何实际出错的情况……

致谢

感谢 Yury Selivanov、Armin Rigo 和 Carl Friedrich Bolz 对此想法早期版本的有益讨论。


来源:https://github.com/python/peps/blob/main/peps/pep-0533.rst

最后修改:2025-02-01 08:59:27 GMT