PEP 469 – 字典迭代代码迁移到 Python 3
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建:
- 2014年4月18日
- Python 版本:
- 3.5
- 历史记录:
- 2014年4月18日,2014年4月21日
摘要
对于 Python 3,PEP 3106 改变了 dict
内置函数和映射 API 的设计,以替换 Python 2 中独立的基于列表和基于迭代器的 API,使用合并的、内存高效的基于集合和多集合视图的 API。这种新的字典迭代风格也作为一组新的迭代方法添加到 Python 2.7 的 dict
类型中。
这意味着现在有 3 种不同的字典迭代方式,当应用程序进行转换时可能需要迁移到 Python 3
- 列表作为可变快照:
d.items()
->list(d.items())
- 迭代器对象:
d.iteritems()
->iter(d.items())
- 基于集合的动态视图:
d.viewitems()
->d.items()
目前还没有广泛认可的最佳实践来可靠地将所有 Python 2 字典迭代代码转换为 Python 2 和 3 的公共子集,尤其是在移植代码的测试覆盖率有限的情况下。本 PEP 审查了访问 Python 2 迭代 API 的各种方法,并研究了通过 Python 2.6+ 和 Python 3.0+ 的公共子集将这些代码迁移到 Python 3 的可用选项。
本 PEP 还考虑了是否值得对 Python 3.5 进行任何添加,这些添加可能会简化应用程序代码的转换过程,这些代码在最终迁移到 Python 3 时无需担心支持早期版本。
PEP 撤回
在撰写本 PEP 的第二稿时,我得出的结论是,混合 Python 2/3 映射代码的可读性实际上可以通过更好的辅助函数来增强,而不是通过对 Python 3.5+ 进行更改来增强。我现在认为本 PEP 的主要价值在于,它清晰地记录了将映射迭代代码从 Python 2 迁移到 Python 3 的推荐方法,并建议了在编写支持这两个版本的混合代码时保持代码可读性和可维护性的方法。
值得注意的是,我建议混合代码避免直接调用映射迭代方法,而是尽可能依赖内置函数,以及一些额外的辅助函数,这些函数在纯 Python 3 代码中是内置函数和映射方法的简单组合,但需要稍微不同的处理方式才能在 Python 2 中获得完全相同的语义。
像 pylint 这样的静态代码检查器可以扩展为可选地警告在混合代码库中直接使用映射迭代方法。
映射迭代模型
Python 2.7 提供了三组不同的方法来从 dict
实例中提取键、值和项,占 dict
类型的 18 个公共方法中的 9 个。
在 Python 3 中,这已被简化为 11 个公共方法中的 3 个(因为 has_key
方法也被删除了)。
列表作为可变快照
这是三种字典迭代风格中最古老的一种,因此也是 Python 2 中 d.keys()
、d.values()
和 d.items()
方法实现的一种。
这些方法都返回列表,这些列表是调用方法时映射状态的快照。这有一些后果
- 可以自由地修改原始对象,而不会影响对快照的迭代
- 可以独立于原始对象修改快照
- 快照消耗的内存与原始映射的大小成正比
这些操作在 Python 3 中的语义等价物是 list(d.keys())
、list(d.values())
和 list(d.iteritems())
。
迭代器对象
在 Python 2.2 中,dict
对象获得了对当时新的迭代器协议的支持,允许直接迭代字典中存储的键,从而避免了仅为了逐个迭代字典内容而构建列表的需要。iter(d)
提供对键的迭代器对象的直接访问。
Python 2 还提供了一个 d.iterkeys()
方法,它基本上与 iter(d)
同义,以及 d.itervalues()
和 d.iteritems()
方法。
这些迭代器提供底层对象的实时视图,因此如果在迭代期间底层对象的键集发生更改,则可能会失败
>>> d = dict(a=1)
>>> for k in d:
... del d[k]
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
作为迭代器,对这些对象的迭代也是一次性操作:迭代器耗尽后,必须返回到原始映射才能再次迭代。
在 Python 3 中,对映射的直接迭代与 Python 2 中的方式相同。没有基于方法的等价物——Python 3 中 d.itervalues()
和 d.iteritems()
的语义等价物是 iter(d.values())
和 iter(d.items())
。
six
和 future.utils
兼容性模块都提供 iterkeys()
、itervalues()
和 iteritems()
辅助函数,这些函数在 Python 2 和 3 中都提供高效的迭代器语义。
基于集合的动态视图
Python 3 中作为基于方法的 API 提供的模型是基于集合的动态视图(在 values()
视图的情况下,技术上是多集合)。
在 Python 3 中,d.keys()
、d.values()
和 d. items()
返回的对象提供了底层对象的当前状态的实时视图,而不是像 Python 2 中那样获取当前状态的完整快照。这种更改在许多情况下都是安全的,但确实意味着,与直接迭代 API 一样,有必要避免在迭代期间添加或删除键,以避免遇到以下错误
>>> d = dict(a=1)
>>> for k, v in d.items():
... del d[k]
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
与迭代 API 不同,这些对象是可迭代的,而不是迭代器:可以多次迭代它们,并且每次它们都会迭代整个底层映射。
这些语义在 Python 2.7 中也可用,作为 d.viewkeys()
、d.viewvalues()
和 d.viewitems()
方法。
future.utils
兼容性模块还在 Python 2.7 或 Python 3.x 上运行时提供 viewkeys()
、viewvalues()
和 viewitems()
辅助函数。
直接迁移到 Python 3
2to3
迁移工具根据上面描述的语义等价物处理直接迁移到 Python 3
d.keys()
->list(d.keys())
d.values()
->list(d.values())
d.items()
->list(d.items())
d.iterkeys()
->iter(d.keys())
d.itervalues()
->iter(d.values())
d.iteritems()
->iter(d.items())
d.viewkeys()
->d.keys()
d.viewvalues()
->d.values()
d.viewitems()
->d.items()
现在只有 3 种视图方法,而不是 9 种用于迭代的不同的映射方法,这些方法以简单的方式与两个相关的内置函数结合,涵盖了作为 Python 2.7 中的 dict
方法可用的一切行为。
请注意,在许多情况下,d.keys()
可以替换为 d
,但 2to3
迁移工具不会尝试进行这种替换。
2to3
迁移工具也不会为将这些对象作为绑定或未绑定方法的引用进行迁移提供任何自动帮助——它只自动化 API 被立即调用的转换。
迁移到 Python 2 和 3 的公共子集
当迁移到 Python 2 和 3 的公共子集时,上述转换通常不适用,因为它们要么导致在 Python 2 中创建冗余列表,要么在某些情况下具有意外的不同语义,或者两者兼而有之。
由于大多数在 Python 2 和 3 的公共子集中运行的代码至少支持 Python 2.6 及更高版本,因此目前推荐的映射迭代操作转换方法依赖于两个辅助函数,以便高效地迭代映射值和映射项元组。
d.keys()
->list(d)
d.values()
->list(itervalues(d))
d.items()
->list(iteritems(d))
d.iterkeys()
->iter(d)
d.itervalues()
->itervalues(d)
d.iteritems()
->iteritems(d)
six
和 future.utils
都提供了 itervalues()
和 iteritems()
的适当定义(以及基本上冗余的 iterkeys()
定义)。在自定义兼容性模块中创建这些函数的自定义定义也相对简单。
try:
dict.iteritems
except AttributeError:
# Python 3
def itervalues(d):
return iter(d.values())
def iteritems(d):
return iter(d.items())
else:
# Python 2
def itervalues(d):
return d.itervalues()
def iteritems(d):
return d.iteritems()
目前,最大的可读性损失出现在转换实际上需要 Python 2 中默认的基于列表的快照的代码时。通过提供 listvalues
和 listitems
辅助函数,可以缓解这种可读性损失,从而使受影响的转换简化为
d.values()
->listvalues(d)
d.items()
->listitems(d)
相应的兼容性函数定义与其迭代器对应部分一样简单。
try:
dict.iteritems
except AttributeError:
# Python 3
def listvalues(d):
return list(d.values())
def listitems(d):
return list(d.items())
else:
# Python 2
def listvalues(d):
return d.values()
def listitems(d):
return d.items()
使用扩展后的兼容性函数集,Python 2 代码将转换为“惯用”的混合 2/3 代码,如下所示:
d.keys()
->list(d)
d.values()
->listvalues(d)
d.items()
->listitems(d)
d.iterkeys()
->iter(d)
d.itervalues()
->itervalues(d)
d.iteritems()
->iteritems(d)
这与直接使用映射方法和内置函数的惯用纯 Python 3 代码的可读性相比,效果很好。
d.keys()
->list(d)
d.values()
->list(d.values())
d.items()
->list(d.items())
d.iterkeys()
->iter(d)
d.itervalues()
->iter(d.values())
d.iteritems()
->iter(d.items())
值得注意的是,当使用这种方法时,混合代码**永远不会**直接调用映射方法:它始终会调用内置函数或辅助函数,以确保在 Python 2 和 3 上具有完全相同的语义。
从 Python 3 迁移到 Python 2.7 的公共子集
虽然目前大多数迁移都是从 Python 2 直接迁移到 Python 3 或 Python 2 和 Python 3 的公共子集,但也有一些较新项目的迁移是从 Python 3 开始,然后由于用户需求或访问 Python 2 库(这些库在 Python 3 中尚不可用,并且将它们移植到 Python 3 或创建 Python 3 兼容的替换版本并非易事)而随后添加 Python 2 支持。
在这些情况下,Python 2.7 兼容性通常就足够了,并且 future.utils
提供的仅限 2.7+ 的基于视图的辅助函数允许将对 Python 3 映射视图方法的裸访问替换为与 Python 2.7 和 Python 3 都兼容的代码(注意,这是 PEP 中唯一一个将 Python 3 代码放在转换左侧的迁移图表)。
d.keys()
->viewkeys(d)
d.values()
->viewvalues(d)
d.items()
->viewitems(d)
list(d.keys())
->list(d)
list(d.values())
->listvalues(d)
list(d.items())
->listitems(d)
iter(d.keys())
->iter(d)
iter(d.values())
->itervalues(d)
iter(d.items())
->iteritems(d)
与从 Python 2 迁移到公共子集一样,请注意混合代码最终永远不会直接调用映射方法 - 它只调用内置函数和辅助方法,后者解决了 Python 2 和 Python 3 之间的语义差异。
Python 3.5+ 可能的更改
为了帮助将现有的 Python 2 代码迁移到 Python 3,提出的主要建议是将一些或所有备用迭代 API 恢复到 Python 3 映射 API 中。特别是,本 PEP 的初稿建议在迁移到 Python 2 和 Python 3.5+ 的公共子集时,使以下转换成为可能。
d.keys()
->list(d)
d.values()
->list(d.itervalues())
d.items()
->list(d.iteritems())
d.iterkeys()
->d.iterkeys()
d.itervalues()
->d.itervalues()
d.iteritems()
->d.iteritems()
恢复这些方法在 Python 3 中创建的额外语言复杂性的可能缓解措施包括立即弃用它们,以及可能将其隐藏在 dir()
函数中(或者甚至定义一种方法使 pydoc
了解函数弃用)。
但是,在实际需要列表输出的情况下,该提案的最终结果实际上不如适当定义的辅助函数更易读,并且从可读性的角度来看,迭代器版本的函数和方法形式几乎相同。
因此,除非我遗漏了某些关键内容,否则现成的 listvalues()
和 listitems()
辅助函数看起来比我们可以添加到 Python 3.5+ 映射 API 中的任何内容都能更好地提高混合代码的可读性,并且不会对 Python 3 本身的复杂性产生任何长期影响。
讨论
在 Python 3 迁移 5 年后,我们仍然有用户认为字典 API 更改是迁移的一个重大障碍,这表明之前推荐的方法存在问题。本 PEP 试图探讨这些问题,并尝试隔离之前建议(如果有的话)可能存在问题的那些情况。
我的评估(主要基于 Twisted 开发人员的反馈)是,当尝试在混合代码中使用 d.keys()
、d.values()
和 d.items()
时,最有可能出现问题。虽然表面上看起来似乎在某些情况下可以安全地忽略语义差异,但实际上,“可变快照”到“动态视图”的更改非常重要,因此最好强制混合代码使用列表或迭代器语义,并将视图语义的使用留给纯 Python 3 代码。
这种方法还创建了足够简单和安全的规则,因此应该可以在针对 Python 2 和 Python 3 公共子集的代码现代化脚本中自动执行这些规则,就像 2to3
在针对纯 Python 3 代码时自动转换它们一样。
致谢
感谢 PyCon 上 Twisted sprint 团队对这个想法(以及其他几个主题)进行了非常热烈的讨论,尤其感谢 Hynek Schlawack 在事情变得有点过热时担任主持人 :)
还要感谢 JP Calderone 和 Itamar Turner-Trauring 提供的电子邮件反馈,以及参与 PEP 初始版本python-dev 审查 的参与者。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0469.rst
上次修改时间:2023-10-11 12:05:51 GMT