Skip to main content

将Python 2代码移植到Python 3

author:

布雷特·加农

抽象

Python 3是Python的未来,而Python 2仍然在使用中,所以你的项目可以用于主要版本的Python。本指南旨在帮助您了解如何最好地同时支持Python 2和3。

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

如果你想阅读一个核心的Python开发人员为什么Python 3成立,你可以阅读Nick Coghlan的 Python 3 Q & A

有关移植的帮助,您可以向 python-porting 邮件列表发送电子邮件问题。

简短说明

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

  1. 只担心支持Python 2.7

  2. 确保你有良好的测试覆盖(coverage.py 可以帮助; pip install coverage

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

  4. 使用 ModernizeFuturize 更新您的代码(分别为 pip install modernizepip install future

  5. 使用 Pylint 帮助确保你不会退回你的Python 3支持(pip install pylint

  6. 使用 caniusepython3 找出哪些依赖项阻止您使用Python 3(pip install caniusepython3

  7. 一旦你的依赖不再阻塞你,使用连续集成,以确保你保持兼容Python 2&3(tox 可以帮助测试多个版本的Python; pip install tox

如果你完全放弃对Python 2的支持,那么在学习了Python 2和3之间的差异之后,您可以对代码运行 2to3,并跳过上面概述的其余步骤。

细节

同时支持Python 2和3的一个关键点是,您可以启动 今天!即使你的依赖不支持Python 3,这并不意味着你不能现代化你的代码 现在 支持Python 3.支持Python 3所需的大多数更改导致更干净的代码使用更新的做法,即使在Python 2。

另一个关键点是,现代化您的Python 2代码也支持Python 3是很大程度上为您自动化。虽然你可能需要做一些API决定,由于Python 3澄清文本数据和二进制数据,较低级别的工作现在主要是为你完成,因此可以立即从自动更改中获益。

在阅读关于移植代码以同时支持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和更早版本,那么对你的代码所需的更改应该继续看起来和感觉像惯用的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版本,你想要确保你的测试套件有良好的覆盖。一个好的经验法则是,如果你想在测试套件中有足够的信心,那么在工具重写代码之后出现的任何故障都是工具中的实际错误,而不是代码中的错误。如果你想要一个数字的目标,尝试超过80%的覆盖(如果你不能容易地超过90%不感觉不好)。如果您还没有一个工具来测量测试覆盖率,那么建议使用 coverage.py

了解Python 2和3之间的区别

一旦你的代码经过测试,你就可以开始将你的代码移植到Python 3!但是要完全理解你的代码将如何改变,你想要看看你的代码,你会想要了解Python 3在Python 2方面的变化。通常,这两个最好的方法是阅读 “What’s New” doc为每个版本的Python 3和 Porting to Python 3 的书(这是免费的在线)。 Python-Future项目中还有一个方便的 cheat sheet

更新您的代码

一旦你觉得你知道与Python 2相比,Python 3有什么不同,是时候更新你的代码!在自动移植代码时,您可以选择两种工具:ModernizeFuturize。你选择哪个工具将取决于你想要代码多少像Python 3。 Futurize 尽力使Python 3的成语和实践存在于Python 2中,例如从Python 3向后移植 bytes 类型,以便在Python的主要版本之间具有语义校验。另一方面,Modernize 更保守,并且目标是Python的Python 2/3子集,依靠 six 来帮助提供兼容性。

无论您选择哪种工具,他们都会将您的代码更新为在Python 3下运行,同时保持与您开始使用的Python 2版本兼容。根据你想要的保守程度,你可能希望首先在你的测试套件上运行该工具,目视检查diff,以确保转换是准确的。在您转换测试套件并验证所有测试仍然按预期通过后,您可以转换您的应用程序代码,因为任何失败的测试都是翻译失败。

不幸的是,工具不能自动化的一切,使您的代码在Python 3下工作,所以有一些事情,你需要手动更新以获得完整的Python 3支持(这些步骤是必要的,不同的工具之间)。阅读您选择使用的工具的文档,以查看默认情况下修复的内容,以及它可以选择性地知道什么(不是)为您修复,以及您可能需要自己修复什么(例如使用 io.open() 内置 open() 功能在Modernize中默认关闭)。幸运的是,只有几件事情要注意,它可以被认为是大问题,如果不监视可能很难调试。

在Python 3中,5 / 2 == 2.5 而不是 2int 值之间的所有划分产生 float。这个更改实际上是从2002年发布的Python 2.2开始计划的。从那时起,鼓励用户将 from __future__ import division 添加到使用 /// 操作符的任何和所有文件,或者运行带有 -Q 标志的解释器。如果你还没有这样做,那么你需要通过你的代码,并做两件事:

  1. from __future__ import division 添加到您的文件

  2. 根据需要更新任何除法运算符,以使用 // 使用floor除法或继续使用 / 并期望浮点

/ 不是简单地转换为 // 自动的原因是,如果一个对象定义一个 __truediv__ 方法,但不是 __floordiv__,那么你的代码将开始失败(例如,一个用户定义的类使用 / 来表示一些操作,但不是 // 的同样的事情或所有)。

文本与二进制数据

在Python 2中,您可以对文本和二进制数据使用 str 类型。不幸的是,两个不同概念的汇合可能导致脆弱的代码,有时工作于任何一种数据,有时不是。如果人们没有明确声明接受 str 的东西接受文本或二进制数据而不是一种特定类型,那么它也可能导致混淆API。这使得情况特别复杂,特别是对于支持多种语言的任何人来说,API在声明文本数据支持时不会麻烦地明确支持 unicode

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

开始,你需要决定哪些API接受文本和哪些接受二进制(这是 高度 推荐你不设计API,可以采取两者,因为难以保持代码工作;如前所述很难做好)。在Python 2中,这意味着确保采用文本的API可以在Python 2中使用 unicode,而使用二进制数据的API与来自Python 3的 bytes 类型一起使用,因此在Python 2中使用 str 的一个子集(在Python 2中使用 bytes 类型2是别名for)。通常最大的问题是同时存在哪些类型的Python 2和3中的类型(对于Python 2中的 unicode 和对于Python 3中的 str 的文本,对于二进制,在Python 2中是 str/bytes,在Python 3中是 bytes)。下表列出了跨Python 2和3的每种数据类型的 独特 方法(例如,decode() 方法可用于Python 2或3中的等价二进制数据类型,但不能一致地由文本数据类型使用在Python 2和3之间,因为 str 在Python 3没有方法)。请注意,从Python 3.5开始,__mod__ 方法被添加到字节类型。

文本数据

二进制数据

解码

编码

 

格式

 

isdecimal

 

isnumeric

 

使得区分更容易处理可以通过在代码的边缘处的二进制数据和文本之间的编码和解码来实现。这意味着,当您接收二进制数据中的文本时,应立即对其进行解码。如果你的代码需要发送文本作为二进制数据,然后尽可能晚的编码。这允许您的代码只在内部使用文本,因此消除了跟踪您正在使用的数据类型。

下一个问题是确保您知道代码中的字符串文字是否代表文本或二进制数据。至少应该在任何提供二进制数据的文本中添加 b 前缀。对于文本,您应该使用 from __future__ import unicode_literals 语句或在文本文本中添加 u 前缀。

作为这种二分法的一部分,你还需要小心打开文件。除非你一直在Windows上工作,有一个机会,当你打开一个二进制文件(例如,rb 二进制读取)时,你并不总是麻烦添加 b 模式。在Python 3下,二进制文件和文本文件明显不同,并且相互不兼容;有关详细信息,请参阅 io 模块。因此,必须 决定文件是否将用于二进制访问(允许读取和/或写入二进制数据)或文本访问(允许读取和/或写入文本数据)。你还应该使用 io.open() 打开文件,而不是内置的 open() 函数,因为 io 模块从Python 2到3是一致的,而内置的 open() 函数不是(在Python 3它实际上是 io.open())。

strbytes 的构造函数对于Python 2和3之间的相同参数具有不同的语义。在Python 2中将整数传递给 bytes 将给出整数的字符串表示形式:bytes(3) == '3'。但在Python 3中,bytes 的整数参数将给你一个字节对象,只要指定的整数,填充空字节:bytes(3) == b'\x00\x00\x00'。当将字节对象传递给 str 时,需要类似的担心。在Python 2中,你只需要得到字节对象:str(b'3') == b'3'。但在Python 3中,你得到字节对象的字符串表示: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)

总结:

  1. 决定哪些API接受文本,哪些接受二进制数据

  2. 确保与文本一起使用的代码也适用于 unicode,并且二进制数据的代码与Python 2中的 bytes 一起使用(请参阅上表,您不能为每种类型使用哪些方法)

  3. 使用 b 前缀标记所有二进制文字,对文本文字使用 u 前缀或 __future__ import语句

  4. 尽快将二进制数据解码为文本,尽可能将文本编码为二进制数据

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

  6. 在索引二进制数据时要小心

使用功能检测而不是版本检测

不可避免的,你将有代码,必须选择基于什么版本的Python正在运行做什么。执行此操作的最佳方法是使用功能检测您运行的Python版本是否支持您所需要的。如果由于某些原因不工作,那么你应该使版本检查是针对Python 2而不是Python 3.为了帮助解释这一点,让我们看一个例子。

让我们假设你需要访问 importlib 的一个特性,该特性从Python 3.3开始就可以在Python的标准库中使用,并且可以通过PyPI上的 importlib2 在Python 2中使用。您可能会试图编写代码以访问 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 3更加兼容于Python 2:

import sys

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

然而,最好的解决方案是完全不进行版本检测,而是依赖于特征检测。这避免了任何潜在的问题的版本检测错误,并帮助保持未来兼容:

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
from __future__ import unicode_literals

您还可以运行带有 -3 标志的Python 2,以警告您的代码在执行期间触发的各种兼容性问题。如果您使用 -Werror 将警告转为错误,那么您可以确保您不会意外错过警告。

您还可以使用 Pylint 项目及其 --py3k 标志来清除代码,以便在代码开始偏离Python 3兼容性时接收警告。这也防止你必须定期运行 ModernizeFuturize 来捕获兼容性回归。这需要你只支持Python 2.7和Python 3.4或更新版本,因为这是Pylint的最低Python版本支持。

检查哪些依赖项阻止了您的转换

你已经使你的代码与Python 3兼容,你应该开始关心你的依赖项是否也已被移植。创建 caniusepython3 项目是为了帮助您确定哪些项目(直接或间接)阻止您支持Python 3.在 https://caniusepython3.com 中有命令行工具以及Web界面。

该项目还提供了可以集成到测试套件中的代码,以便在没有依赖关系阻止您使用Python 3时,您将进行失败的测试。这允许您避免手动检查依赖关系并快速通知当你可以开始运行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.5开始可用)比较时,您可能还想使用 -bb 标志与Python 3解释器来触发异常。默认情况下,类型不同的比较只返回 False,但如果你在分离文本/二进制数据处理或字节索引时犯了一个错误,你不会轻易找到错误。当发生这种类型的比较时,这个标志将引发异常,使得错误更容易跟踪。

这就是大多数!在这一点上,您的代码库与Python 2和3同时兼容。您的测试也将被设置,以便您不会意外打破Python 2或3兼容性,无论哪个版本,您通常在开发时运行测试。

完全删除Python 2支持

如果你能够完全删除对Python 2的支持,那么转换到Python 3所需的步骤就会大大简化。

  1. 更新您的代码只支持Python 2.7

  2. 确保你有良好的测试覆盖(coverage.py 可以帮助)

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

  4. 使用 2to3 重写代码以仅在Python 3下运行

之后,你的代码将完全符合Python 3,但是在Python 2不支持的方式。你也应该更新你的 setup.py 中的分类器包含 Programming Language :: Python :: 3 :: Only