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
在许多方面是一个更复杂、更专业的实用程序:它承担了填充缺失值的责任,这是其他变体都不需要担心的工作。
如果 zip
和 zip_longest
都并存在 itertools
中或作为内置函数,那么在同一位置添加 zip_strict
确实会是一个更有力的论据。但是,新的“strict”变体在接口和行为上在概念上与 zip
非常接近,而不是 zip_longest
,同时仍然没有达到成为自己的内置函数的高标准。鉴于这种情况,zip
在原地增加这个新选项似乎最自然。
可用性
如果 zip
能够防止此类错误,那么用户使用此属性在调用站点启用检查变得简单得多。将其与导入内置实用程序的直接替换进行比较,这感觉有点沉重,仅仅是为了检查一个应该“始终”为真的棘手条件。
有些人还认为,标准库中隐藏的新函数比内置函数本身的关键字参数更容易“发现”。作者不同意这种评估。
维护成本
虽然在进行可用性改进时,实现应该只是一个次要的考虑因素,但重要的是要认识到,添加新的实用程序比修改现有的实用程序复杂得多。此 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 和语义。
不做任何事
此选项可能最不吸引人。
静默截断数据是一种特别讨厌的错误类型,并且手工编写一个能够正确处理此问题的健壮解决方案并非易事。来自 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
上次修改时间:2023-09-09 17:39:29 GMT