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 递归函数详解及实例
Dec 27 Python
Python中取整的几种方法小结
Jan 06 Python
关于Python数据结构中字典的心得
Dec 04 Python
python爬取各类文档方法归类汇总
Mar 22 Python
深入分析python数据挖掘 Json结构分析
Apr 21 Python
Python基于最小二乘法实现曲线拟合示例
Jun 14 Python
Pycharm配置远程调试的方法步骤
Dec 17 Python
python三方库之requests的快速上手
Mar 04 Python
Django网络框架之创建虚拟开发环境操作示例
Jun 06 Python
Python如何将字符串转换为日期
Jul 31 Python
Pandas||过滤缺失数据||pd.dropna()函数的用法说明
May 14 Python
pytorch锁死在dataloader(训练时卡死)
May 28 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
PHP Header用于页面跳转要注意的几个问题总结
2008/10/03 PHP
PHP新手入门学习方法
2011/05/08 PHP
PHP中spl_autoload_register函数的用法总结
2013/11/07 PHP
javascript同步Import,同步调用外部js的方法
2008/07/08 Javascript
js textarea自动增高并隐藏滚动条
2009/12/16 Javascript
Javascript学习笔记之 对象篇(三) : hasOwnProperty
2014/06/24 Javascript
Javascript中arguments对象详解
2014/10/22 Javascript
JS实现保留n位小数的四舍五入问题示例
2016/08/03 Javascript
jQuery中的select操作详解
2016/11/29 Javascript
node.js实现登录注册页面
2017/04/08 Javascript
JavaScript简介_动力节点Java学院整理
2017/06/26 Javascript
从源码看angular/material2 中 dialog模块的实现方法
2017/10/18 Javascript
nodejs高大上的部署方式(PM2)
2018/09/11 NodeJs
Jquery实现无缝向上循环滚动列表的特效
2019/02/13 jQuery
微信小程序实现获取用户信息并存入数据库操作示例
2019/05/07 Javascript
细说webpack6 Babel的使用详解
2019/09/26 Javascript
浅谈Vue2.4.0 $attrs与inheritAttrs的具体使用
2020/03/08 Javascript
jquery实现加载更多"转圈圈"效果(示例代码)
2020/11/09 jQuery
JS实现页面侧边栏效果探究
2021/01/08 Javascript
Python中使用logging模块打印log日志详解
2015/04/05 Python
详解python发送各类邮件的主要方法
2016/12/22 Python
Python正则表达式实现简易计算器功能示例
2019/05/07 Python
Python常用模块之requests模块用法分析
2019/05/15 Python
Win10系统下安装labelme及json文件批量转化方法
2019/07/30 Python
pymysql模块的使用(增删改查)详解
2019/09/09 Python
python list数据等间隔抽取并新建list存储的例子
2019/11/27 Python
Python numpy.zero() 初始化矩阵实例
2019/11/27 Python
基于Tensorflow读取MNIST数据集时网络超时的解决方式
2020/06/22 Python
会计专业应届生求职信
2013/11/24 职场文书
教师绩效工资方案
2014/02/01 职场文书
婚纱摄影师求职信
2014/03/07 职场文书
基层工作经验证明样本
2014/11/16 职场文书
先进基层党组织事迹材料
2014/12/25 职场文书
毕业论文致谢词
2015/05/14 职场文书
基于Pygame实现简单的贪吃蛇游戏
2021/12/06 Python
SQL Server中常用截取字符串函数介绍
2022/03/16 SQL Server