Python 3.10 的首个 PEP 诞生,内置类型 zip() 迎来新特性
> 译者前言:相信凡是用过 zip() 内置函数的人,都会赞同它很有用,但是,它的最大问题是可能会产生出非预期的结果。PEP-618 提出给它增加一个参数,可以有效地解决大家的痛点。 > > 这是 Python 3.10 版本正式采纳的第一个 PEP,「Python猫」一直有跟进社区最新动态的习惯,所以翻译了出来给大家尝鲜,强烈推荐一读。(PS:严格来说,zip() 是一个内置类(built-in type),而不是一个内置函数(built-in function),但我们一般都称它为一个内置函数。)
PEP原文 : https://www.python.org/dev/peps/pep-0618/
PEP标题: Add Optional Length-Checking To zip
PEP作者: Brandt Bucher
创建日期: 2020-05-01
合入版本: 3.10
译者 :豌豆花下猫 @Python猫公众号
PEP翻译计划 :https://github.com/chinesehuazhou/peps-cn
摘要
本 PEP 建议给内置的 zip
添加一个可选的 strict 布尔关键字参数。当启用时,如果其中一个参数先被用尽了,则会引发 ValueError 。
动机
从作者的个人经验和一份对标准库的调查 来看,明显有很多(如果不是绝大多数)zip 用例要求可迭代对象必须是等长的。有时候,周围代码的上下文可以保证这点,但是要 zip 处理的数据通常是由调用者传入的、单独提供的或者以某种方式生成的。在这些情况下,zip 的默认行为意味着错误的重构或逻辑错误,很容易悄悄地导致数据丢失。这些 bug 不仅难以定位,甚至难以被觉察到。
很容易想到造成这种问题的简单案例。例如,以下代码在 items 为一个序列(sequence)时可以良好地运行,但是如果调用者将 item 重构为一个可消耗的迭代器,则代码会悄悄地产生缩短的、不匹配的结果:
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 的倍数的数据通常也是错误。因为这两个习惯用法都会悄悄地忽略不匹配的尾部元素。
最有说服力的例子来自使用了 zip 的标准库ast
,它在 literal_eval 里产生过一个 bug,会直接丢弃不匹配的节点:
>>> from ast import Constant, Dict, literal_eval >>> nasty_dict = Dict(keys=[Constant(None)], values=[]) >>> literal_eval(nasty_dict) # Like eval("{None: }") {}
实际上,笔者已经在 Python 的标准库和工具中找出了许多调用点, 立即在这些位置启用此新特性是恰当的。
基本原理
一些评论者声称:布尔开关常量是一种“代码坏气味(code-smell)”,或者与 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)
被拒绝的意见
(1)添加 itertools.zip_strict
这是 Python-Ideas 邮件列表上获得最多支持的替代方案,因此值得在此处加以讨论。它没有任何严重的缺陷,如果本 PEP 被否绝,它是一个很好的替代。
虽然考虑到这一点,但是在 zip 中添加可选参数可以用较小的更改而更好地解决诱发此 PEP 的问题。
(2)依照先例
itertools 中有一个 zip_longest,这似乎让人很有动机再添加一个 zip_strict。但是,zip_longest 在许多方面是一个更加复杂且特定的程序:它负责填写缺失的值,但其它函数都不需要操心这种事。
如果 zip 和 zip_longest 同时放在 itertools 中,或者都作为内置函数,那么在相同的地方添加 zip_strict 就确实是一个更有效的论点。然而,新的“strict”用法在接口和行为方面,相比起 zip_longest,更接近于 zip 的概念,但又不足以成为内置对象。考虑到这个原因,令 zip 就地扩展出一个新的选项,似乎是最自然的选择。
(3)易用性
如果 zip 能够防止此类 bug,那么用户在调用的地方启动检查,就会变得非常简单。与其编写一套繁重的逻辑来处理,不如用这个新特性来直接检查。
有人还认为,在标准库中放一个新的函数,相比在一个内置函数上加关键字参数,更“容易发现(discoverable)”。笔者不同意这一论断。
(4)维护成本
尽管在提升易用性时,具体的实现是个次要问题,但重要的是要认识到,添加新的程序比修改原有程序复杂得多。与此 PEP 一起提供的 CPython 实现非常简单,并且对 zip 的默认行为没有显著的性能影响,而在 itertools 中添加一个全新的程序将需要:
- 复制 zip 的许多现有逻辑,zip_longest 就是这么干的。
- 大刀阔斧地重构 zip 或 zip_longest 或这两者,以便共享一个公共的或者继承性的实现(这可能会影响性能)。
(5)添加多个“模式”以供切换
如果预期有三个或更多模式(mode),这个建议才会比二元标志更有意义。最显而易见的三种模式是:“最短的”(当前 zip 的行为),“严格的”(本 PEP 提议的行为)和“最长的”(itertools.zip_longest 的行为)。
但是,除了当前的默认值以及本提案的“strict”模式,似乎不需要再添加其它模式。最可能的是添加一个“最长的”模式,但这需要一个新的 fillvalue 参数(它对于前两种模式都没有意义),另外,itertools.zip_longest 已经完美地处理了这种模式,若在 zip 中添加该模式,将会造成重复。目前尚不清楚哪一个是“显而易见的”选择:内置 zip 上的 mode 参数,还是已经长期存在于 itertools 中的 zip_longest。
(6)给 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 内置类的实现细节是未文档化的。这意味着若选择以上的某种行为,当前的实现就会被“锁定”(或至少要求对其进行仿真)。
(7)变更 zip 的默认行为
zip 的默认行为没有什么“错” ,因为在许多情况下,这确实是正确处理大小不等的输入的方法。例如,在处理无限迭代器时,它非常有用。
itertools.zip_longest 已经用在仍然需要“额外”尾端数据的情况。
(8)使用回调来处理剩余对象
尽管基本上可以执行用户需要的任何操作,但此解决方案在处理常见问题时(例如舍弃不匹配的长度),变得不必要的复杂且不直观。
(9)引发一个 AssertionError
没有内置函数或内置类的 API 会引发 AssertionError。此外,官方文档 这么写的(它的全部):
> Raised when an assert
statement fails.
由于此功能与 Python 的 assert 语句无关,因此不应该引发 AssertionError。用户若希望在优化模式下禁用检查(像一个 assert 语句),可以改用 strict = __debug__。
(10)在 map 上添加类似的特性
本 PEP 不建议对 map 作任何更改,因为很少使用带有多个可迭代参数的map。但是,本 PEP 的裁定可作为将来讨论类似特性的先例(应该出现)。
如果本 PEP 被拒绝,则 map 的那种特性实际上也不值得追求。如果通过了,则对 map 的更改不需要新的 PEP(尽管像所有提案一样,都应仔细考虑其有用性)。为了保持一致性,它应遵循此处讨论的跟 zip 相同的 API 和语义。
(11)什么也不做
此建议可能最没有吸引力。
悄悄地将数据截断是一种特别令人讨厌的 bug,而手写一个健壮的解决方案却并非易事。Python 自己的标准库(前文提到的 ast)是有现实意义的反例,很容易就陷入本 PEP 试图避免的那种陷阱。
推荐阅读:
1、PEP中文翻译计划 (https://github.com/chinesehuazhou/peps-cn)
2、学习 Python,怎能不懂点PEP呢? (https://mp.weixin.qq.com/s/oRoBxZ2-IyuPOf_MWyKZyw)
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
iOS14新特性探索之一:App Clips轻应用
iOS14新特性探索之一:App Clips轻应用 App Clips是苹果WWDC 2020所发布的iOS 14新特性中最具焦点的一项功能。一经曝光,就引发了互联网上针对其特性的各种讨论。有人说App Clips是苹果模仿微信退出的iOS平台的小程序;有人说它是轻量级的应用程序,为用户提供了简洁版的App体验;同样,对AppClips的评价也是众说纷纭,有人看好也有人看跌,有人觉得是新的平台也有人觉得非常鸡肋。 无论如何,AppClips都是Apple给iPhone用户提供了一种新的交互方式和新的应用使用体验,作为开发者,我们更需要做的是了解这样一种新的技术的应用,并将其赋能到我们的产品中,为用户提供更好的使用体验,为产品带来更大的价值。 本篇博客,也是基于这样的想法,将全面的介绍App Clips的应用与开发细节,帮助大家最快的了解与上手这样一种技术。我在编写本篇博客时,使用的依然是iOS14的bate版本,开发工具Xcode的版本也是12.0Bate版本,因此,不能保证后续Apple不会对App Clips的某些特性进行优化修改。如果你在之后很久的某个时间阅...
- 下一篇
公司短信平台上的2万块钱,瞬间就被黑光了
我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农! 文章会收录在 JavaNewBee ,还要更多文章合集。 前两天的中午像往常一样热,太阳不知疲倦的在天空燃烧,热跑了云彩和鸟儿,马上就要点燃空气和我的脑神经。为我和电脑降温的,是我简陋的书桌上的小电扇,没有它的话,键盘太热,我可能就要写不下去代码了。 正在此时,旁边的手机嗡嗡的震了两声,对于手机从来不敢开铃声的人来说,这个震动的声音实在太熟悉了,不用说,应该是广告短信,或者有人加我微信好友了。因为短信我基本上从来不看,微信消息不会有提示,只有加好友才有,由于最近有不少朋友看到我写的文章,所以每天加我好友的还是不少的,我都是找空闲时间统一处理。所以,我还是继续写我的代码,没有理会。 过了两分钟左右,嗡嗡~~又震了两声,不慌,继续写代码。然后嗡嗡 ~~ 又震了两声,接着又震了两声,我心想,难道又是哪个大号转了我文章了(心里略带几分得意),淡定,继续写代码。 这时候已经持续了 6、7次,我刚要拿手机看一下,突然有事儿,赶紧开门出去了,过了20分钟回来之后,发现手机还在震。我赶紧拿起来一看...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Red5直播服务器,属于Java语言的直播服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS6,CentOS7官方镜像安装Oracle11G
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16