将 Python 2 代码移植到 Python 3

  • author

    • Brett Cannon

Abstract

随着 Python 3 成为 Python 的 Future,而 Python 2 仍在积极使用中,让您的项目适用于 Python 的两个主要版本是一件好事。本指南旨在帮助您确定如何最好地同时支持 Python 2 和 3.

如果要移植扩展模块而不是纯 Python 代码,请参阅将扩展模块移植到 Python 3

如果您想阅读 Python 的一位核心开发人员对 Python 3 为何产生的看法,您可以阅读 Nick Coghlan 的Python 3 问答或 Brett Cannon 的为什么存在 Python 3

要获得移植方面的帮助,您可以pass电子邮件向python-porting邮件列表发送问题。

简短说明

为了使您的项目与 Python 2/3 单一源兼容,基本步骤是:

  • 只担心支持 Python 2.7

  • 确保您具有良好的测试覆盖范围(coverage.py可以提供帮助; pip install coverage)

  • 了解 Python 2 和 3 之间的区别

  • 使用Futurize(或Modernize)更新您的代码(例如pip install future)

  • 使用Pylint帮助确保您不退一步获得对 Python 3 的支持(pip install pylint)

  • 使用caniusepython3查找哪些依赖项阻止了您对 Python 3(pip install caniusepython3)的使用

  • 一旦您的依赖关系不再受阻,请使用持续集成以确保与 Python 2 和 3 保持兼容(tox可帮助测试多个版本的 Python; pip install tox)

  • 考虑使用可选的静态类型检查,以确保您的类型用法在 Python 2 和 3 中都可以使用(例如,使用mypy检查在 Python 2 和 Python 3 中的键入)。

Details

同时支持 Python 2 和 3 的一个关键点是您可以 今天开始 !即使您的依赖项不支持 Python 3,这也不意味着您不能立即更新代码以支持 Python3.支持 Python 3 所需的大多数更改即使使用 Python 2 代码也可以使用较新的方法来使代码更简洁。 。

另一个关键点是,现代化 Python 2 代码以同时支持 Python 3 对您来说是自动化的。尽管您可能需要借助 Python 3 来澄清文本数据和二进制数据,才能做出一些 API 决定,但现在大部分工作已由您完成,因此至少可以立即受益于自动更改。

在 continue 阅读有关移植代码以同时支持 Python 2 和 3 的详细信息时,请记住这些关键点。

放弃对 Python 2.6 和更早版本的支持

虽然您可以使 Python 2.5 与 Python 3 一起使用,但如果您只需要与 Python 2.7 一起使用,它就会“容易得多”。如果不能选择放弃 Python 2.5,则six项目可以帮助您同时支持 Python 2.5 和 3(pip install six)。但是请务必意识到,该 HOWTO 中列出的几乎所有项目都将对您不可用。

如果您可以跳过 Python 2.5 和更早的版本,则对代码进行的所需更改应 continue 看起来和惯用的 Python 代码一样。在最坏的情况下,在某些情况下,您将不得不使用函数而不是方法,或者必须导入函数而不是使用内置函数,但是否则,整个转换对您而言不会感到陌生。

但是您应该只支持 Python 2.7. 不再免费支持 Python 2.6,因此不会收到错误修复。这意味着**您将必须解决 Python 2.6 遇到的任何问题。本 HOWTO 中还提到了一些不支持 Python 2.6 的工具(例如Pylint),随着时间的流逝,这将变得越来越普遍。如果仅支持必须支持的 Python 版本,这对您来说将变得更加简单。

确保在 setup.py 文件中指定了正确的版本支持

setup.py文件中,您应具有正确的trove classifier,以指定您支持的 Python 版本。由于您的项目不支持 Python 3,因此您至少应指定Programming Language :: Python :: 2 :: Only。理想情况下,您还应该指定您支持的每个主要/次要版本的 Python,例如Programming Language :: Python :: 2.7

具有良好的测试覆盖率

一旦您的代码支持了您想要的最旧版本的 Python 2,就将要确保您的测试套件具有良好的覆盖率。一个好的经验法则是,如果您对测试套件有足够的信心,那么在使用工具重写代码后出现的任何故障都是工具中的实际错误,而不是代码中的错误。如果您想要一个数字作为目标,请try获得 80%以上的覆盖率(如果发现很难获得超过 90%的覆盖率,也不要感到难过)。如果您还没有衡量测试覆盖率的工具,那么建议使用coverage.py

了解 Python 2 和 3 之间的区别

一旦对代码进行了充分的测试,就可以开始将代码移植到 Python 3 了!但是,要完全了解您的代码将如何变化以及在编写代码时要注意什么,您将需要学习 Python 3 就 Python 2 所做的更改。通常,两种最佳的读取方法是阅读每种 Python 3 版本的"What's New"文档和移植到 Python 3书(在线免费)。 Python-Future 项目中还有一个方便的cheat sheet

更新您的代码

一旦您知道 Python 3 与 Python 2 有什么不同,就该更新代码了!您可以选择两种工具来自动移植代码:FuturizeModernize。选择哪种工具将取决于您希望代码成为 Python 3 的程度。 Futurize尽最大努力使 Python 3 习惯用法和实践存在于 Python 2 中,例如从 Python 3 向后移植bytes类型,以便在主要版本的 Python 之间具有语义奇偶校验。另一方面,Modernize较为保守,针对 Python 的 Python 2/3 子集,直接依赖six来帮助提供兼容性。随着 Python 3 的 FutureDeveloping,最好考虑使用 Futurize 来开始适应 Python 3 引入的您还不习惯的任何新实践。

无论您选择哪种工具,他们都会更新您的代码以在 Python 3 下运行,同时与您最初使用的 Python 2 版本保持兼容。根据您希望保持的保守程度,您可能希望首先在测试套件上运行该工具,然后目视检查差异以确保转换正确。转换完测试套件并确认所有测试仍按预期pass后,您可以转换应用程序代码,知道任何失败的测试都是翻译失败。

不幸的是,这些工具不能自动完成所有工作以使您的代码在 Python 3 下工作,因此您需要手动进行一些更新才能获得完整的 Python 3 支持(这些步骤中的哪些步骤因工具而异)。阅读您选择使用的工具的文档,以查看默认情况下可以修复的Function以及可以选择执行的操作,以了解哪些将为您(不)修复,以及您可能需要自己修复的(例如,使用io.open()内置open()函数在 Modernize 中默认为关闭)。幸运的是,只有几件事需要注意,可以将其视为大问题,如果不加以注意可能很难调试。

Division

在 Python 3 中,5 / 2 == 2.5而不是2int值之间的所有除法都得出float。从 2002 年发布 Python 2.2 开始,实际上已经在计划这一更改。从那时起,我们鼓励用户将from __future__ import division添加到使用///运算符的任何文件中,或使用-Q标志运行解释器。如果您尚未执行此操作,则需要遍历代码并执行以下两项操作:

  • from __future__ import division添加到您的文件

  • 根据需要更新任何除法运算符,以使用//进行地板除法或 continue 使用/并期望有浮点数

/不能简单地自动转换为//的原因是,如果对象定义了__truediv__方法而不是__floordiv__,则您的代码将开始失败(例如,用户定义的类使用/表示某些操作,但不使用//表示相同或完全相同)。

文本与二进制数据

在 Python 2 中,您可以将str类型用于文本和二进制数据。不幸的是,两个不同概念的融合可能导致脆弱的代码有时对两种数据都起作用,有时却不起作用。如果人们没有明确语句接受str的对象接受了文本或二进制数据而不是一种特定类型的数据,那么这也可能导致 API 混乱。这使情况变得更加复杂,尤其是对于那些支持多种语言的人,因为当他们声称支持文本数据时,API 不会显式地支持unicode

为了使文本和二进制数据之间的区别更清晰,更明确,Python 3 做了互联网时代大多数语言创建的语言,使文本和二进制数据成为了无法盲目地混在一起的不同类型(Python 早已广泛访问了互联网)。对于仅处理文本或仅处理二进制数据的任何代码,这种分隔都不会造成问题。但是对于必须同时处理这两者的代码,这确实意味着您可能现在必须关心与二进制数据相比何时使用文本,这就是为什么不能完全自动化的原因。

首先,您需要确定哪些 API 使用文本,哪些 API 使用二进制文件(强烈建议您不要设计同时使用这两种 API 的 API,因为它们难以保持代码正常工作;如前所述)很难做好)。在 Python 2 中,这意味着确保采用文本的 API 可以与unicode一起使用,并且确保处理二进制数据的 API 与 Python 3 中的bytes类型一起使用(这是 Python 2 中str的子集,并充当bytes类型的别名) Python 2)。通常,最大的问题是要同时意识到在 Python 2 和 3 中的哪种类型上同时存在哪些方法(对于 Python 2 中的unicode和 Python 3 中的str文本,对于 Python 2 中的str/bytes和 Python 3 的bytes二进制)。下表列出了 Python 2 和 3 中每种数据类型的 unique 方法(例如decode()方法可用于 Python 2 或 3 中的等效二进制数据类型,但不能被decode()方法使用)。 Python 2 和 3 之间一致的文本数据类型,因为 Python 3 中的str没有该方法)。请注意,从 Python 3.5 开始,__mod__方法已添加到字节类型。

Text dataBinary data
decode
encode
format
isdecimal
isnumeric

pass在代码边缘的二进制数据和文本之间进行编码和解码,可以使区分更易于处理。这意味着当您接收二进制数据中的文本时,应立即对其进行解码。并且,如果您的代码需要将文本作为二进制数据发送,则应尽可能晚地对其进行编码。这样一来,您的代码就只能在内部使用文本,从而无需跟踪正在处理的数据类型。

下一个问题是确保您知道代码中的字符串 Literals 代表的是文本还是二进制数据。您应该在提供二进制数据的任何 Literals 上添加b前缀。对于文本,应在文本 Literals 上添加u前缀。 (passfuture导入可将所有未指定的 Literals 强制为 Unicode,但用法表明,它不如在所有 Literals 上显式添加bu前缀那样有效)

作为这种二分法的一部分,您还需要注意打开文件的注意事项。除非您在 Windows 上工作过,否则打开二进制文件(例如rb进行二进制读取)时,您不一定总是会费心添加b模式。在 Python 3 中,二进制文件和文本文件明显不同且互不兼容。有关详情,请参见io模块。因此,您必须**决定是将文件用于二进制访问(允许读取和/或写入二进制数据)还是文本访问(允许读取和/或写入文本数据)。您还应该使用io.open()而不是内置open()函数来打开文件,因为io模块在 Python 2 至 3 中是一致的,而内置open()函数则不是(在 Python 3 中实际上是io.open())。不要为使用codecs.open()过时的做法而烦恼,因为只有这样才能保持与 Python 2.5 的兼容性。

strbytes的构造函数对 Python 2 和 3 之间的相同参数具有不同的语义。在 Python 2 中将整数传递给bytes将为您提供整数的字符串表示形式:bytes(3) == '3'。但是在 Python 3 中,bytes的整数参数将为您提供一个字节对象,只要指定的整数(用空字节bytes(3) == b'\x00\x00\x00'填充)即可。将字节对象传递给str时,也需要类似的担心。在 Python 2 中,您只需返回 bytes 对象:str(b'3') == b'3'。但是在 Python 3 中,您将获得 bytes 对象的字符串表示形式:str(b'3') == "b'3'"

最后,对二进制数据构建索引需要仔细处理(切片**不需要任何特殊处理)。在 Python 2 中,b'123'[1] == b'2'而在 Python 3 b'123'[1] == 50中。因为二进制数据只是二进制数的集合,所以 Python 3 返回您索引的字节的整数值。但是在 Python 2 中,由于bytes == str,索引返回一个单项字节片。 six项目有一个名为six.indexbytes()的函数,该函数将返回一个整数,类似于 Python 3:six.indexbytes(b'123', 1)

To summarize:

  • 确定您的哪个 API 使用文本,哪些使用二进制数据

  • 确保在 Python 2 中与文本一起使用的代码也与unicode一起使用,而二进制数据的代码也与bytes一起使用(有关每种类型不能使用的方法,请参见上表)

  • 将所有二进制 Literals 标记为b前缀,将 Literals 文本标记为u前缀

  • 尽快将二进制数据解码为文本,尽快将文本编码为二进制数据

  • 使用io.open()打开文件,并确保在适当时指定b模式

  • 索引到二进制数据时要小心

使用Function检测代替版本检测

不可避免地,您将拥有必须根据正在运行的 Python 版本选择要执行的操作的代码。最好的方法是passFunction检测您正在运行的 Python 版本是否支持所需的Function。如果由于某种原因不起作用,则应针对 Python 2 而不是 Python 3 进行版本检查。为帮助解释这一点,让我们看一个示例。

假设您需要访问importlib的Function,此Function自 Python 3.3 起在 Python 的标准库中可用,并且在 PyPI 上可用于 Python 2 到importlib2。您可能会想编写代码来访问例如importlib.abc模块,方法如下:

import sys

if sys.version_info[0] == 3:
    from importlib import abc
else:
    from importlib2 import abc

这段代码的问题是当 Python 4 发布时会发生什么?最好将 Python 2 视为特殊情况,而不是 Python 3,并假设将来的 Python 版本将比 Python 2 与 Python 3 更兼容:

import sys

if sys.version_info[0] > 2:
    from importlib import abc
else:
    from importlib2 import abc

不过,最好的解决方案是完全不进行版本检测,而要依靠Function检测。这样可以避免任何可能导致版本检测错误的潜在问题,并帮助您保持将来的兼容性:

try:
    from importlib import abc
except ImportError:
    from importlib2 import abc

防止兼容性下降

完全翻译完代码以使其与 Python 3 兼容后,您将要确保代码不会退化并停止在 Python 3 下运行。如果您有依赖项阻止您实际在以下环境下运行,则尤其如此。目前使用 Python 3.

为了保持兼容性,您创建的任何新模块的顶部至少应包含以下代码块:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

您还可以使用-3标志运行 Python 2,以警告您的代码在执行过程中触发的各种兼容性问题。如果您将警告变成-Werror的错误,则可以确保您不会意外遗漏警告。

当您的代码开始偏离 Python 3 兼容性时,您还可以使用Pylint项目及其--py3k标志来使您的代码收起代码来接收警告。这也避免了您必须定期在代码上运行ModernizeFuturize来捕获兼容性回归。这确实要求您仅支持 Python 2.7 和 Python 3.4 或更高版本,因为这是 Pylint 的最低 Python 版本支持。

检查哪些依赖项阻碍了您的过渡

使代码与 Python 3 兼容之后,您应该开始考虑是否已经移植了您的依赖项。创建caniusepython3项目是为了帮助您确定哪些项目(直接或间接)使您无法支持 Python3.在https://caniusepython3.com处既有命令行工具又有 Web 界面。

该项目还提供了可以集成到测试套件中的代码,这样当您不再具有依赖项而无法使用 Python 3 时,您将无法pass测试。这使您避免手动检查依赖项并迅速得到通知。当您可以开始在 Python 3 上运行时。

更新您的 setup.py 文件以表示 Python 3 兼容性

一旦您的代码在 Python 3 下工作,您就应该更新setup.py中的分类器以包含Programming Language :: Python :: 3,并且不要指定唯一的 Python 2 支持。这将告诉使用您的代码的任何人您都支持 Python 2 3.理想情况下,您还希望为现在支持的每个主要/次要版本的 Python 添加分类器。

使用持续集成来保持兼容性

一旦能够在 Python 3 下完全运行,您将需要确保代码始终在 Python 2 和 3 下都能正常工作。在多个 Python 解释器下运行测试的最佳工具可能是tox。然后,您可以将 tox 与您的持续集成系统集成在一起,这样就不会意外破坏 Python 2 或 3 支持。

当您比较字节和字符串或字节和 int 时,您可能还想在 Python 3 解释器中使用-bb标志来触发异常(后者从 Python 3.5 开始可用)。默认情况下,类型不同的比较仅返回False,但是如果您在文本/二进制数据处理或对字节的索引分离中犯了一个错误,则不会轻易发现错误。发生此类比较时,此标志将引发异常,从而使错误更容易查找。

大部分就是这样!此时,您的代码库同时与 Python 2 和 3 兼容。还将对您的测试进行设置,以确保您在开发过程中通常不会在哪个版本下运行测试,都不会意外破坏 Python 2 或 3 的兼容性。

考虑使用可选的静态类型检查

帮助移植代码的另一种方法是在代码上使用诸如mypypytype之类的静态类型检查器。这些工具可用于分析您的代码,就像它在 Python 2 下运行一样。然后,您可以再次运行该工具,就像您的代码在 Python 3 下运行一样。pass这样两次运行静态类型检查器,您可以发现是否你就是与另一个版本相比,在一个版本的 Python 中滥用二进制数据类型。如果您在代码中添加了可选的类型提示,则还可以显式语句您的 API 使用的是文本数据还是二进制数据,从而帮助确保所有Function在这两个 Python 版本中都能正常运行。