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

Python 增强提案

PEP 618 – 为 zip 添加可选长度检查

作者:
Brandt Bucher <brandt at python.org>
赞助人:
Antoine Pitrou <antoine at python.org>
BDFL 代表:
Guido van Rossum <guido at python.org>
状态:
最终
类型:
标准跟踪
创建:
2020 年 5 月 1 日
Python 版本:
3.10
历史记录:
2020 年 5 月 1 日,2020 年 5 月 10 日,2020 年 6 月 16 日
决议:
Python-Dev 消息

目录

摘要

此 PEP 提出为内置的 zip 添加一个可选的 strict 布尔关键字参数。启用后,如果其中一个参数在其他参数之前耗尽,则会引发 ValueError

动机

从作者的个人经验和对标准库的调查可以清楚地看出,许多(如果不是大多数)zip 用例都涉及必须长度相等的迭代器。有时,此不变性从周围代码的上下文中得到证明,但通常被压缩的数据是从调用方传递的、分别来源的或以某种方式生成的。在任何这些情况下,zip 的默认行为意味着错误的重构或逻辑错误很容易导致静默丢失数据。这些错误不仅难以诊断,而且甚至难以检测。

很容易想出一些这可能成为问题的简单案例。例如,以下代码在 items 是序列时可能工作正常,但如果 items 由调用方重构为可消耗的迭代器,则会静默开始产生缩短的、不匹配的结果

def apply_calculations(items):
    transformed = transform(items)
    for i, t in zip(items, transformed):
        yield calculate(i, t)

还有其他几种 zip 的常用方式。惯用的技巧特别容易受到影响,因为它们通常被缺乏对代码工作原理的完整理解的用户使用。一个例子是解包到 zip 以延迟“解压缩”或“转置”嵌套的迭代器

>>> x = [[1, 2, 3], ["one" "two" "three"]]
>>> xt = list(zip(*x))

另一个是将数据“分块”成大小相等的分组

>>> n = 3
>>> x = range(n ** 2),
>>> xn = list(zip(*[iter(x)] * n))

在第一种情况下,非矩形数据通常是逻辑错误。在第二种情况下,长度不是 n 的倍数的数据通常也是错误。但是,这两种习惯用法都会静默省略格式错误输入的尾部项目。

也许最令人信服的是,标准库 ast 模块中 zip 的使用在 literal_eval 中产生了错误,该错误静默丢弃了格式错误节点的部分内容

>>> from ast import Constant, Dict, literal_eval
>>> nasty_dict = Dict(keys=[Constant(None)], values=[])
>>> literal_eval(nasty_dict)  # Like eval("{None: }")
{}

事实上,作者已统计了 Python 的标准库和工具中数十个其他调用站点,在这些站点中,立即启用此新功能将是合适的。

基本原理

一些批评人士断言,常量布尔开关是“代码异味”,或者违反了 Python 的设计理念。但是,Python 目前包含内置函数中布尔关键字参数的几个示例,这些函数通常使用编译时常量调用

  • compile(..., dont_inherit=True)
  • open(..., closefd=False)
  • print(..., flush=True)
  • sorted(..., reverse=True)

标准库中还有更多。

此新参数的想法和名称最初是由 Ram Rachum提议的。该主题收到了超过 100 条回复,其中备选方案“equal”获得了类似的支持。

作者对这两个选择没有强烈偏好,尽管“equal equals”在散文方面确实有点笨拙。它也可能(错误地)暗示了压缩项目之间某种“相等”的概念

>>> z = zip([2.0, 4.0, 6.0], [2, 4, 8], equal=True)

规范

当内置的 zip 使用仅限关键字的参数 strict=True 调用时,如果参数以不同的长度耗尽,则生成的迭代器将引发 ValueError。此错误将在迭代通常在今天停止的点发生。

向后兼容性

此更改完全向后兼容。zip 目前不接受任何关键字参数,并且省略 strict 时的“非严格”默认行为保持不变。

参考实现

作者起草了一个C 实现

一个近似的 Python 翻译是

def zip(*iterables, strict=False):
    if not iterables:
        return
    iterators = tuple(iter(iterable) for iterable in iterables)
    try:
        while True:
            items = []
            for iterator in iterators:
                items.append(next(iterator))
            yield tuple(items)
    except StopIteration:
        if not strict:
            return
    if items:
        i = len(items)
        plural = " " if i == 1 else "s 1-"
        msg = f"zip() argument {i+1} is shorter than argument{plural}{i}"
        raise ValueError(msg)
    sentinel = object()
    for i, iterator in enumerate(iterators[1:], 1):
        if next(iterator, sentinel) is not sentinel:
            plural = " " if i == 1 else "s 1-"
            msg = f"zip() argument {i+1} is longer than argument{plural}{i}"
            raise ValueError(msg)

被拒绝的想法

添加 itertools.zip_strict

这是 Python-Ideas 邮件列表中获得最多支持的替代方案,因此值得在这里详细讨论。它没有任何取消资格的缺陷,并且可以作为此 PEP 被拒绝时的替代方案。

考虑到这一点,本节旨在概述为什么为 zip 添加可选参数是一个较小的更改,最终在解决激励此 PEP 的问题方面做得更好。

先例

似乎推动这种替代方案的很大一部分动机是 itertools 中已经存在 zip_longest。但是,zip_longest 在许多方面是一个更复杂、更专业的实用程序:它承担了填充缺失值的责任,这是其他变体都不需要担心的工作。

如果 zipzip_longest 都并存在 itertools 中或作为内置函数,那么在同一位置添加 zip_strict 确实会是一个更有力的论据。但是,新的“strict”变体在接口和行为上在概念上与 zip 非常接近,而不是 zip_longest,同时仍然没有达到成为自己的内置函数的高标准。鉴于这种情况,zip 在原地增加这个新选项似乎最自然。

可用性

如果 zip 能够防止此类错误,那么用户使用此属性在调用站点启用检查变得简单得多。将其与导入内置实用程序的直接替换进行比较,这感觉有点沉重,仅仅是为了检查一个应该“始终”为真的棘手条件。

有些人还认为,标准库中隐藏的新函数比内置函数本身的关键字参数更容易“发现”。作者不同意这种评估。

维护成本

虽然在进行可用性改进时,实现应该只是一个次要的考虑因素,但重要的是要认识到,添加新的实用程序比修改现有的实用程序复杂得多。此 PEP 附带的 CPython 实现很简单,并且对默认的 zip 行为没有可衡量的性能影响,而为 itertools 添加一个全新的实用程序则需要

  • 复制大部分现有的 zip 逻辑,就像 zip_longest 已经做的那样。
  • zipzip_longest 或两者进行重大重构以共享一个公共或继承的实现(这可能会影响性能)。

添加多个“模式”在它们之间切换

只有当我们预计有三种或更多模式时,此选项才会比二进制标志更有意义。这三种枚举或常量模式的“明显”选择将是“最短”(当前的 zip 行为),“严格”(建议的行为)和“最长”(itertools.zip_longest 行为)。

但是,添加除当前默认值和建议的“严格”模式之外的其他行为似乎不值得额外的复杂性。“最长”最明显的候选者将需要一个新的 fillvalue 参数(这对其他两种模式都没有意义)。此模式也已由 itertools.zip_longest 完美处理,添加它将创建两种执行相同操作的方法。目前尚不清楚哪一个是“明显的”选择:内置 zip 上的 mode 参数,还是 itertools 中的长期同名实用程序。

zip 类型添加方法或备用构造函数

考虑以下两个选项,这两个选项都已被提议

>>> zm = zip(*iters).strict()
>>> zd = zip.strict(*iters)

目前尚不清楚哪一个会成功,或者另一个会如何失败。如果 zip.strict 实现为方法,则 zm 将成功,但 zd 将以多种令人困惑的方式之一失败

  • 产生未包装在元组中的结果(如果 iters 只包含一个项目,一个 zip 迭代器)。
  • 对不正确的参数类型引发 TypeError(如果 iters 只包含一个项目,而不是 zip 迭代器)。
  • 对不正确的参数数量引发 TypeError(否则)。

如果 zip.strict 被实现为 classmethodstaticmethod,则 zd 会成功,而 zm 将静默地不产生任何输出(这正是我们一开始试图避免的问题)。

这个提案还因为 CPython 的实际 zip 类型目前是一个未公开的实现细节而变得更加复杂。这意味着选择上述行为之一实际上会“锁定”当前的实现(或者至少需要对其进行模拟)以供将来使用。

更改 zip 的默认行为

zip 的默认行为并没有什么“错误”,因为在许多情况下,它确实是处理大小不一的输入的正确方法。例如,在处理无限迭代器时,它非常有用。

itertools.zip_longest 已经存在,用于处理仍然需要“额外”尾部数据的那些情况。

接受一个回调来处理剩余的项目

虽然能够做到用户可能需要的任何事情,但这种解决方案使得处理更常见的情况(例如拒绝不匹配的长度)变得不必要地复杂且不明显。

引发 AssertionError

没有内置函数或类型在其 API 中引发 AssertionError。此外,官方文档 的全部内容只是

assert 语句失败时引发。

由于此功能与 Python 的 assert 语句无关,因此在此处引发 AssertionError 是不合适的。希望进行在优化模式下禁用的检查(如 assert 语句)的用户可以使用 strict=__debug__ 代替。

map 添加类似的功能

本 PEP 没有提出对 map 进行任何更改,因为使用多个可迭代参数的 map 非常罕见。但是,本 PEP 的裁决将作为此类未来讨论(如果发生)的先例。

如果被拒绝,则该功能实际上不值得追求。如果被接受,对 map 的此类更改不需要其自己的 PEP(尽管,像所有增强功能一样,应仔细考虑其有用性)。为了保持一致性,它应该遵循此处针对 zip 讨论的相同 API 和语义。

不做任何事

此选项可能最不吸引人。

静默截断数据是一种特别讨厌的错误类型,并且手工编写一个能够正确处理此问题的健壮解决方案并非易事。来自 Python 自身标准库的真实世界激励示例证明,很容易陷入此功能旨在避免的那种陷阱。

参考文献

示例

注意

此列表并不详尽。


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

上次修改时间:2023-09-09 17:39:29 GMT