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 的数据是从调用者传递的、单独获取的或以某种方式生成的。在任何这些情况下,zip 的默认行为意味着错误的重构或逻辑错误可能很容易导致数据悄悄丢失。这些 bug 不仅难以诊断,而且甚至难以检测。
很容易想到可能出现此问题的简单案例。例如,以下代码在 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 中造成了一个 bug,该 bug 悄悄地丢弃了格式错误的节点部分
>>> 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”在散文中确实有点笨拙。它可能还会(错误地)暗示被 zip 的项之间存在某种“相等”概念
>>> z = zip([2.0, 4.0, 6.0], [2, 4, 8], equal=True)
规范
当使用仅限关键字参数 strict=True 调用内置函数 zip 时,如果参数在不同长度处耗尽,则生成的迭代器将引发 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 在许多方面是一个更复杂、更专业的实用程序:它承担了填充缺失值的责任,而其他两种变体都不需要关心这项工作。
如果 zip 和 zip_longest 都存在于 itertools 中或作为内置函数,那么在同一位置添加 zip_strict 确实会是一个更有力的论据。然而,新的“严格”变体在接口和行为上概念上*非常*接近 zip,但仍未达到成为其自身内置函数的高标准。鉴于这种情况,zip 在原地增加这个新选项似乎最自然。
可用性
如果 zip 能够阻止这类 bug,那么用户在具有此属性的调用点启用检查就会简单得多。这与导入内置实用程序的替代品相比,感觉有些沉重,仅仅是为了检查一个“应该总是”为真的棘手条件。
一些人还认为,标准库中深藏的新函数比内置函数上的关键字参数更“易于发现”。作者不认同这一评估。
维护成本
虽然在进行可用性改进时,实现应该只是次要考虑,但认识到添加新实用程序比修改现有实用程序复杂得多是很重要的。随本 PEP 提供的 CPython 实现很简单,对默认的 zip 行为没有可测量的性能影响,而向 itertools 添加一个全新的实用程序将需要
- 重复大部分现有
zip逻辑,就像zip_longest已经做的那样。 - 显著重构
zip、zip_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 作为 classmethod 或 staticmethod 实现,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 和语义。
不做任何更改
此选项可能是最不吸引人的。
静默截断数据是一种特别讨厌的 bug,手动编写一个能够正确处理此问题的健壮解决方案并非易事。来自 Python 自己的标准库的真实世界案例证明,陷入此功能旨在避免的陷阱*非常*容易。
参考资料
示例
注意
此列表并非详尽无遗。
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/_pydecimal.py#L3394
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/_pydecimal.py#L3418
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/_pydecimal.py#L3435
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L94-L95
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1184
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1275
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1363
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1391
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/copy.py#L217
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/csv.py#L142
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/dis.py#L462
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/filecmp.py#L142
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/filecmp.py#L143
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/inspect.py#L1440
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/inspect.py#L2095
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/os.py#L510
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/plistlib.py#L577
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/tarfile.py#L1317
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/tarfile.py#L1323
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/tarfile.py#L1339
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/turtle.py#L3015
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/turtle.py#L3071
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/turtle.py#L3901
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0618.rst