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二叉搜索树与双向链表转换实现方法
Apr 29 Python
Python使用pymysql小技巧
Jun 04 Python
python中将函数赋值给变量时需要注意的一些问题
Aug 18 Python
Python使用matplotlib实现绘制自定义图形功能示例
Jan 18 Python
详解python实现线程安全的单例模式
Mar 05 Python
Python pyinotify日志监控系统处理日志的方法
Mar 08 Python
python使用tornado实现简单爬虫
Jul 28 Python
python3+opencv3识别图片中的物体并截取的方法
Dec 05 Python
python 画函数曲线示例
Dec 04 Python
python3中使用__slots__限定实例属性操作分析
Feb 14 Python
基于Django OneToOneField和ForeignKey的区别详解
Mar 30 Python
Pytorch使用PIL和Numpy将单张图片转为Pytorch张量方式
May 25 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读取ACCESS数据到MYSQL的代码
2011/05/11 PHP
浅析ThinkPHP中execute和query方法的区别
2014/06/13 PHP
PHP常用字符串操作函数实例总结(trim、nl2br、addcslashes、uudecode、md5等)
2016/01/09 PHP
PHP类型约束用法示例
2016/09/28 PHP
php文件上传及下载附带显示文件及目录功能
2017/04/27 PHP
JavaScript 图片预览效果 推荐
2009/12/22 Javascript
JavaScript 盒模型 尺寸深入理解
2012/12/31 Javascript
简介JavaScript中的getSeconds()方法的使用
2015/06/10 Javascript
jquery ajax分页插件的简单实现
2016/01/27 Javascript
BootStrap Progressbar 实现大文件上传的进度条的实例代码
2016/06/27 Javascript
基于Vue2实现的仿手机QQ单页面应用功能(接入聊天机器人 )
2017/03/30 Javascript
详解Angular 4.x NgTemplateOutlet
2017/05/24 Javascript
Angular2环境搭建具体操作步骤(推荐)
2017/08/04 Javascript
基于Node.js的大文件分片上传示例
2019/06/19 Javascript
layui异步加载table表中某一列数据的例子
2019/09/16 Javascript
javascript设计模式 ? 适配器模式原理与应用实例分析
2020/04/13 Javascript
浅谈nuxtjs校验登录中间件和混入(mixin)
2020/11/06 Javascript
Python 数据结构之队列的实现
2017/01/22 Python
Python 快速实现CLI 应用程序的脚手架
2017/12/05 Python
python实现创建新列表和新字典,并使元素及键值对全部变成小写
2019/01/15 Python
Python获取命令实时输出-原样彩色输出并返回输出结果的示例
2019/07/11 Python
Django 项目重命名的实现步骤解析
2019/08/14 Python
pytorch 常用线性函数详解
2020/01/15 Python
Python生成并下载文件后端代码实例
2020/08/31 Python
Python 高效编程技巧分享
2020/09/10 Python
用CSS3实现背景渐变的方法
2015/07/14 HTML / CSS
Html5让容器充满屏幕高度或自适应剩余高度的布局实现
2020/05/14 HTML / CSS
什么是Smarty变量操作符?如何使用Smarty变量操作符
2014/07/18 面试题
介绍一下如何利用路径遍历进行攻击及如何防范
2014/01/19 面试题
新闻专业个人自我评价
2013/09/21 职场文书
2014年幼儿园安全工作总结
2014/11/10 职场文书
大班上学期个人总结
2015/02/13 职场文书
付款证明格式范文
2015/06/19 职场文书
使用Golang的channel交叉打印两个数组的操作
2021/04/29 Golang
为什么MySQL选择Repeatable Read作为默认隔离级别
2021/07/26 MySQL
电脑关机速度很慢怎么办 提升电脑关机速度设置教程
2022/04/08 数码科技