Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 的设计,用合并的、内存高效的集合和多重集合视图 API 取代了 Python 2 中单独的基于列表和基于迭代器的 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 3.5+,可以最好地增强混合 Python 2/3 映射代码的可读性。我现在认为本 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())

sixfuture.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() 方法。

当在 Python 2.7 或 Python 3.x 上运行时,future.utils 兼容性模块也提供了 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()

现在不是 9 个不同的映射迭代方法,而只有 3 个视图方法,它们与两个相关的内置函数以直接的方式结合,涵盖了 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)

sixfuture.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 中默认的基于列表快照的代码时。这种可读性损失可以通过提供 listvalueslistitems 辅助函数来缓解,从而将受影响的转换简化为

  • 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 3 中尚未提供的 Python 2 库(并且将其移植到 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 迁移五年后,我们仍然有用户认为 dict API 的变化是迁移的重大障碍,这表明以前推荐的方法存在问题。本 PEP 试图探讨这些问题,并试图隔离以前的建议(如果存在的话)可能存在问题的案例。

我的评估(主要基于 Twisted 开发人员的反馈)是,当尝试在混合代码中使用 d.keys()d.values()d.items() 时,最有可能出现问题。虽然表面上看起来应该存在可以安全忽略语义差异的情况,但实际上,“可变快照”到“动态视图”的变化足够显著,以至于最好强制混合代码使用列表或迭代器语义,并将视图语义留给纯 Python 3 代码。

这种方法还创建了足够简单和安全的规则,应该可以在针对 Python 2 和 Python 3 公共子集的代码现代化脚本中自动化它们,就像 2to3 在针对纯 Python 3 代码时自动转换它们一样。

致谢

感谢 PyCon 上 Twisted 冲刺会议桌的各位同仁对此想法(以及其他几个主题)进行了非常热烈的讨论,特别感谢 Hynek Schlawack 在讨论变得有些过于激烈时充当了主持人 :)

还要感谢 JP Calderone 和 Itamar Turner-Trauring 的电子邮件反馈,以及参与 python-dev 对 PEP 初稿进行审阅 的参与者。


来源: https://github.com/python/peps/blob/main/peps/pep-0469.rst

最后修改: 2025-02-01 08:59:27 GMT