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数组的处理代码
Jan 04 Python
Python psutil模块简单使用实例
Apr 28 Python
Python中条件判断语句的简单使用方法
Aug 21 Python
Python实现以时间换空间的缓存替换算法
Feb 19 Python
Python编程实现的简单Web服务器示例
Jun 22 Python
python如何通过twisted实现数据库异步插入
Mar 20 Python
Python使用tkinter库实现文本显示用户输入功能示例
May 30 Python
Python数据类型之List列表实例详解
May 08 Python
tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this T
Jun 22 Python
浅谈TensorFlow之稀疏张量表示
Jun 30 Python
matplotlib对象拾取事件处理的实现
Jan 14 Python
Python基础之元编程知识总结
May 23 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代码
2007/03/03 PHP
PHP DataGrid 实现代码
2009/08/12 PHP
PHP性能优化准备篇图解PEAR安装
2011/12/05 PHP
关于crontab的使用详解
2013/06/24 PHP
PHP关于htmlspecialchars、strip_tags、addslashes的解释
2014/07/04 PHP
基于ThinkPHP+uploadify+upload+PHPExcel 无刷新导入数据
2015/09/23 PHP
PHP实现的限制IP投票程序IP来源分析
2016/05/04 PHP
PHP通过get方法获得form表单数据方法总结
2018/09/12 PHP
javascript 写类方式之八
2009/07/05 Javascript
ASP.NET jQuery 实例13 原创jQuery文本框字符限制插件-TextArea Counter
2012/02/03 Javascript
ASP.NET jQuery 实例16 通过控件CustomValidator验证RadioButtonList
2012/02/03 Javascript
检查输入的是否是数字使用keyCode配合onkeypress事件
2014/01/23 Javascript
JavaScript中的正则表达式简明总结
2014/04/04 Javascript
jquery防止重复执行动画避免页面混乱
2014/04/22 Javascript
jquery制作 随机弹跳的小球特效
2015/02/01 Javascript
js自定义回调函数
2015/12/13 Javascript
NodeJs——入门必看攻略
2016/06/27 NodeJs
Node.js的文件权限及读写flag详解
2016/10/11 Javascript
微信开发 js实现tabs选项卡效果
2016/10/28 Javascript
jQuery的事件预绑定
2016/12/05 Javascript
基于bootstrap风格的弹框插件
2016/12/28 Javascript
JavaScript & jQuery完美判断图片是否加载完毕
2017/01/08 Javascript
vue.js项目打包上线的图文教程
2017/11/16 Javascript
vue中提示$index is not defined错误的解决方式
2020/09/02 Javascript
html中创建并调用vue组件的几种方法汇总
2020/11/17 Javascript
JS实现点击掉落特效
2021/01/29 Javascript
Python发送Email方法实例
2014/08/21 Python
Python中编写ORM框架的入门指引
2015/04/29 Python
Python读写unicode文件的方法
2015/07/10 Python
CSS3条纹背景制作的实战攻略
2016/05/31 HTML / CSS
来自Ocado的宠物商店:Fetch
2018/07/10 全球购物
环境科学专业个人求职的自我评价
2013/11/28 职场文书
销售简历自我评价怎么写
2014/09/26 职场文书
Pytest allure 命令行参数的使用
2021/04/18 Python
新手入门Jvm-- JVM对象创建与内存分配机制
2021/06/18 Java/Android
SQL Server数据库备份和恢复数据库的全过程
2022/06/14 SQL Server