Python 3.10 的首个 PEP 诞生,内置类型 zip() 迎来新特性(推荐)


Posted in Python onJuly 03, 2020

译者前言:相信凡是用过 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

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)

总结

到此这篇关于Python 3.10 的首个 PEP 诞生,内置类型 zip() 迎来新特性的文章就介绍到这了,更多相关Python 3.10 内置类型 zip() 内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
简单介绍Python的Django框架加载模版的方式
Jul 20 Python
python中文乱码不着急,先看懂字节和字符
Dec 20 Python
python编写Logistic逻辑回归
Dec 30 Python
解决Python网页爬虫之中文乱码问题
May 11 Python
python利用微信公众号实现报警功能
Jun 10 Python
深入了解Python在HDA中的应用
Sep 05 Python
Python实现快速排序的方法详解
Oct 25 Python
Python测试Kafka集群(pykafka)实例
Dec 23 Python
在Python中通过threshold创建mask方式
Feb 19 Python
Python要求O(n)复杂度求无序列表中第K的大元素实例
Apr 02 Python
Django model重写save方法及update踩坑详解
Jul 27 Python
Django url 路由匹配过程详解
Jan 22 Python
python3 简单实现组合设计模式
Jul 02 #Python
Django Session和Cookie分别实现记住用户登录状态操作
Jul 02 #Python
django 装饰器 检测登录状态操作
Jul 02 #Python
详解用Python爬虫获取百度企业信用中企业基本信息
Jul 02 #Python
django 实现后台从富文本提取纯文本
Jul 02 #Python
详解用Python调用百度地图正/逆地理编码API
Jul 02 #Python
基于django2.2连oracle11g解决版本冲突的问题
Jul 02 #Python
You might like
ueditor 1.2.6 使用方法说明
2013/07/24 PHP
yii框架表单模型使用及以数组形式提交表单数据示例
2014/04/30 PHP
浅谈php使用curl模拟多线程发送请求
2019/03/08 PHP
PHP序列化和反序列化深度剖析实例讲解
2020/12/29 PHP
js中call与apply的用法小结
2013/12/28 Javascript
javascript间隔定时器(延时定时器)学习 间隔调用和延时调用
2014/01/13 Javascript
js实现正方形颜色从下往上升的效果
2014/08/04 Javascript
jQuery+css3动画属性制作猎豹浏览器宽屏banner焦点图
2015/03/16 Javascript
jquery实现叠层3D文字特效代码分享
2015/08/21 Javascript
JavaScript实现复制内容到粘贴板代码
2016/03/31 Javascript
jQuery实现简单的网页换肤效果示例
2016/09/18 Javascript
js实现右键自定义菜单
2016/12/03 Javascript
PHP+jquery+ajax实现分页
2016/12/09 Javascript
Angular开发者指南之入门介绍
2017/03/05 Javascript
JS实现颜色的10进制转化成rgba格式的方法
2017/09/04 Javascript
vue.js实现简单轮播图效果
2017/10/10 Javascript
js计算两个日期间的天数月的实例代码
2018/09/20 Javascript
jquery实现弹窗(系统提示框)效果
2019/12/10 jQuery
[03:54]Ehome出征西雅图 回顾2016国际邀请赛晋级之路
2016/08/02 DOTA
[05:23]DOTA2-DPC中国联赛2月1日Recap集锦
2021/03/11 DOTA
Django中使用locals()函数的技巧
2015/07/16 Python
Python编程中time模块的一些关键用法解析
2016/01/19 Python
python存储16bit和32bit图像的实例
2018/12/05 Python
详解Python正则表达式re模块
2019/03/19 Python
Django 开发环境配置过程详解
2019/07/18 Python
Python3进制之间的转换代码实例
2019/08/24 Python
python 如何将office文件转换为PDF
2020/09/22 Python
日本化妆品植村秀俄罗斯官方网站:Shu Uemura俄罗斯
2020/02/01 全球购物
工商管理毕业生推荐信
2013/12/24 职场文书
高中生家长会演讲稿
2014/01/14 职场文书
全陪导游词
2015/02/04 职场文书
画展观后感
2015/06/17 职场文书
2019年大学生职业生涯规划书最新范文
2019/03/25 职场文书
Ajax实现局部刷新的方法实例
2021/03/31 Javascript
Python快速优雅的批量修改Word文档样式
2021/05/20 Python
Java处理延时任务的常用几种解决方案
2022/06/01 Java/Android