将python 2代码移植到python 3

作者

布雷特大炮

摘要

随着python 3成为python的未来,而python 2仍在使用中,让您的项目同时可用于两个主要的python版本是很好的。本指南旨在帮助您了解如何最好地同时支持Python2和3。

如果您希望移植扩展模块而不是纯Python代码,请参见 将扩展模块移植到python 3 .

如果您想阅读一个核心的python开发人员对python 3产生的原因的看法,可以阅读nick coghlan的 Python 3 Q & A 或者布雷特加农的 Why Python 3 exists .

有关移植的帮助,可以通过电子邮件 python-porting 有问题的邮件列表。

简短的解释

要使您的项目与单源python 2/3兼容,基本步骤如下:

  1. 只担心支持python 2.7

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

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

  4. 使用 Futurize (或) Modernize) 更新您的代码(例如 python -m pip install future

  5. 使用 Pylint 以帮助确保您不会在python 3支持上倒退。 (python -m pip install pylint

  6. 使用 caniusepython3 要找出哪些依赖项阻止了您使用python 3 (python -m pip install caniusepython3

  7. 一旦您的依赖不再阻塞您,使用持续集成来确保您与Python2&3保持兼容。 (tox 可以帮助测试多个版本的python; python -m pip install tox

  8. 考虑使用可选的静态类型检查,以确保您的类型使用在python 2和3中都有效(例如,使用 mypy 检查您在python 2和python 3下输入的内容。 python -m pip install mypy

注解

注:使用 python -m pip install 保证 pip 您调用的是当前正在使用的python所安装的,无论它是系统范围的 pip 或安装在 virtual environment .

细节

同时支持python 2&3的一个关键点是,您可以从 今天 !即使您的依赖项还不支持Python3,这并不意味着您不能使代码现代化。 now 以支持python 3。支持python 3所需的大多数更改都会导致使用更新的实践(甚至在python 2代码中)来清理代码。

另一个关键点是,对python 2代码进行现代化以同时支持python 3在很大程度上是自动化的。由于python 3澄清了文本数据和二进制数据,您可能需要做出一些API决策,但是现在底层的工作主要是为您完成的,因此至少可以立即从自动更改中获益。

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

放弃对python 2.6及更旧版本的支持

虽然您可以使python 2.5与python 3一起工作,但它是 much 如果只需要使用python 2.7就更容易了。如果删除python 2.5不是一个选项,那么 six Project可以帮助您同时支持python 2.5和3 (python -m pip install six )不过,请务必认识到,本指南中列出的几乎所有项目都不会提供给您。

如果您能够跳过python 2.5及更旧版本,那么对代码所需的更改应该继续看起来和感觉像是惯用的python代码。在某些情况下,最坏情况下,您将不得不使用函数而不是方法,或者必须导入函数而不是使用内置函数,但否则,整体转换对您来说不应该感到陌生。

但您的目标应该是只支持Python2.7。python 2.6不再受自由支持,因此不接收错误修复。这意味着 you 必须解决您在使用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之间的区别

一旦对代码进行了良好的测试,就可以开始将代码移植到Python3了!但是,为了充分理解代码将如何更改,以及在编写代码时需要注意什么,您需要了解python 3对python 2所做的更改。通常,两种最好的方法是阅读 "What's New" 每次发布python 3和 Porting to Python 3 书籍(在线免费)。还有一个方便 cheat sheet 来自python未来项目。

更新您的代码

一旦你觉得你知道了Python3和Python2的区别,现在是时候更新你的代码了!在自动移植代码时,您可以选择两种工具: FuturizeModernize. 您选择的工具取决于您希望代码与Python3有多相似。 Futurize 尽可能使python 3的习惯用法和实践存在于python 2中,例如 bytes 从python 3中键入,以便在python的主要版本之间具有语义奇偶性。 Modernize, 另一方面,更为保守,目标是Python的2/3子集,直接依赖于 six 以帮助提供兼容性。因为python 3是未来,所以最好考虑未来化,开始适应python 3引入的任何您还不习惯的新实践。

无论您选择哪种工具,它们都会更新您的代码以在Python3下运行,同时与您开始使用的Python2版本保持兼容。根据您希望的保守程度,您可能希望首先在测试套件上运行该工具,并目视检查diff以确保转换是准确的。在您转换了测试套件并验证了所有测试仍按预期通过之后,您就可以转换应用程序代码,知道任何失败的测试都是转换失败。

不幸的是,这些工具不能自动完成所有工作,使您的代码能够在python 3下工作,因此您需要手动更新一些东西,以获得完整的python 3支持(这些步骤中的哪些是必要的,在工具之间有所不同)。阅读您选择使用的工具的文档,了解它默认修复的内容,以及它可以做什么(可选)来了解将为您修复什么(不)以及您可能需要自己修复什么(例如使用 io.open() 在内置的 open() 函数在现代化中默认为关闭状态)。不过,幸运的是,只有两件事需要注意,如果不注意的话,可能会被认为是很难调试的大问题。

除法

在Python 3中, 5 / 2 == 2.5 and not 2; all division between int values result in a float. This change has actually been planned since Python 2.2 which was released in 2002. Since then users have been encouraged to add from __future__ import division to any and all files which use the / and // 操作员或正在使用 -Q flag。如果您没有这样做,那么您需要检查代码并做两件事:

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

  2. 根据需要更新任何除法运算符 // 使用向下取整数或继续使用 / 期望浮动

原因在于 / isn't simply translated to // automatically is that if an object defines a __truediv__ method but not __floordiv__ then your code would begin to fail (e.g. a user-defined class that uses / to signify some operation but not // 为了同样的事情或所有)。

文本与二进制数据

在Python2中,您可以使用 str 文本和二进制数据的类型。不幸的是,这两个不同概念的融合可能导致代码脆弱,有时对任何一种数据都有效,有时则不然。如果人们没有明确地声明接受的东西,它也可能导致混淆API。 str 接受文本或二进制数据,而不是一种特定类型。这使情况变得复杂,尤其是对于支持多种语言的任何人来说,API都不需要显式支持。 unicode 当他们声称支持文本数据时。

为了使文本和二进制数据之间的区别更清晰和更明显,python 3做了互联网时代大多数语言所做的事情,并使文本和二进制数据具有不同的类型,这些类型不能盲目地混合在一起(python早于对互联网的广泛访问)。对于任何只处理文本或二进制数据的代码,这种分离不会造成问题。但是对于必须同时处理这两个问题的代码来说,这确实意味着您现在可能需要关注与二进制数据相比,什么时候使用文本,这就是为什么这不能完全自动化的原因。

首先,您需要决定哪些API采用文本,哪些采用二进制(它是 高度地 建议您不要设计API,因为很难保持代码的工作;如前所述,很难做好这两个方面的工作。在python 2中,这意味着确保获取文本的API可以使用 unicode and those that work with binary data work with the bytes type from Python 3 (which is a subset of str in Python 2 and acts as an alias for bytes type in Python 2). Usually the biggest issue is realizing which methods exist on which types in Python 2 & 3 simultaneously (for text that's unicode in Python 2 and str in Python 3, for binary that's str/bytes 在Python 2和 bytes 在Python 3中)。下表列出了 独特的 python 2&3中每种数据类型的方法(例如 decode() 方法可用于python 2或3中的等效二进制数据类型,但不能由python 2和3之间的文本数据类型一致地使用,因为 str 在python 3中没有这个方法)。请注意,从python 3.5开始, __mod__ 方法已添加到字节类型。

文本数据

二进制数据

译码

编码

格式

十进制的

非数字字符

通过在代码边缘对二进制数据和文本进行编码和解码,可以使区分更容易处理。这意味着当您接收到二进制数据中的文本时,应该立即对其进行解码。如果您的代码需要以二进制数据的形式发送文本,那么请尽可能晚地对其进行编码。这允许您的代码只在内部使用文本,从而消除了跟踪您使用的数据类型的必要性。

下一个问题是确保您知道代码中的字符串文本是表示文本还是二进制数据。你应该加一个 b 表示二进制数据的任何文本的前缀。对于文本,应添加 u 文本文本的前缀。(有一个 __future__ 导入以强制所有未指定的文本为Unicode,但使用表明,它不如添加 bu 所有文字的前缀)

作为这种二分法的一部分,您还需要小心打开文件。除非您一直在使用Windows,否则有可能您并不总是费心添加 b 打开二进制文件时的模式(例如, rb 用于二进制读取)。在python 3中,二进制文件和文本文件明显不同,并且相互不兼容;请参见 io 详细信息模块。因此,你 must 决定文件是用于二进制访问(允许读取和/或写入二进制数据)还是用于文本访问(允许读取和/或写入文本数据)。你也应该使用 io.open() 用于打开文件而不是内置的 open() 作为 io 模块从python 2到3是一致的,而内置的 open() 函数不是(在python 3中,它实际上是 io.open() )不要为过时的使用习惯操心。 codecs.open() 因为这只是保持与Python2.5的兼容性所必需的。

两者的建设者 strbytes 对于Python2和3之间的相同参数具有不同的语义。将整数传递给 bytes 在python 2中,将为您提供整数的字符串表示: bytes(3) == '3' . 但在python 3中,一个整型参数 bytes 将为您提供一个字节对象,只要指定的整数填充空字节: bytes(3) == b'\x00\x00\x00' . 将bytes对象传递给 str . 在python 2中,只需返回bytes对象: str(b'3') == b'3' . 但在python 3中,您可以得到bytes对象的字符串表示: str(b'3') == "b'3'" .

最后,索引二进制数据需要小心处理(切片确实需要 not 需要特殊处理)。在Python 2中, b'123'[1] == b'2' 在python 3中 b'123'[1] == 50 . 因为二进制数据只是二进制数字的集合,所以python 3返回您索引的字节的整数值。但是在Python2中,因为 bytes == str ,indexing返回一个一项字节切片。这个 six 项目具有名为的函数 six.indexbytes() 它将返回一个类似于python 3的整数: six.indexbytes(b'123', 1) .

总结:

  1. 决定哪个API采用文本,哪个采用二进制数据

  2. 确保与文本一起使用的代码也可以与 unicode 二进制数据的代码与 bytes 在python 2中(请参见上表了解您不能为每种类型使用哪些方法)

  3. 用标记所有二进制文本 b 前缀,文本文本文本 u 前缀

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

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

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

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

不可避免地,您将拥有一些代码,这些代码必须根据运行的Python版本来选择要做什么。实现这一点的最佳方法是使用特性检测运行的Python版本是否支持您需要的内容。如果出于某种原因,这不起作用,那么您应该让版本检查针对的是Python2,而不是Python3。为了帮助解释这一点,我们来看一个例子。

假设您需要访问 importlib 这在Python的标准库中提供,从python 3.3开始,一直到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 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

防止兼容性回归

一旦您将代码完全翻译为与Python3兼容,您就需要确保代码不会退化,并且不会停止在Python3下工作。如果您有一个依赖项,而这个依赖项现在正阻碍您在python 3下实际运行,那么这一点尤其正确。

为了有助于保持兼容,您创建的任何新模块顶部都应至少具有以下代码块:

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

您还可以使用 -3 在执行期间要警告代码触发器的各种兼容性问题的标志。如果将警告转换为错误 -Werror 然后你可以确保你不会意外地错过一个警告。

您也可以使用 Pylint 项目及其 --py3k 当代码开始偏离Python3兼容性时,标记为lint您的代码以接收警告。这也可以防止你不得不运行 ModernizeFuturize 定期检查代码以获取兼容性回归。这并不要求您只支持python 2.7和python 3.4或更高版本,因为这是pylint最低的python版本支持。

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

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

该项目还提供了一些代码,您可以将这些代码集成到测试套件中,这样当您不再具有阻止您使用Python3的依赖项时,您将有一个失败的测试。这允许您避免手动检查依赖项,并在可以开始在python 3上运行时快速得到通知。

更新你的 setup.py 表示python 3兼容性的文件

一旦您的代码在python 3下工作,您应该在 setup.py 遏制 Programming Language :: Python :: 3 并且不指定唯一的python 2支持。这将告诉任何使用您的代码的人您支持Python2 and 三。理想情况下,您还需要为现在支持的每个主要/次要版本的Python添加分类器。

使用持续集成保持兼容

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

您也可以使用 -bb 使用python 3解释器标记,以便在将字节与字符串或字节与int进行比较时触发异常(后者从python 3.5开始提供)。默认情况下,类型差异比较只返回 False 但是,如果在文本/二进制数据处理或字节索引的分离中出错,就不容易发现错误。当发生此类比较时,此标志将引发异常,使错误更容易跟踪。

基本上就是这样!此时,您的代码库与python 2和3同时兼容。您的测试也将被设置为不会意外破坏Python2或3的兼容性,无论您在开发时通常在哪个版本下运行测试。

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

另一种帮助移植代码的方法是使用静态类型检查器 mypypytype 你的代码。这些工具可以用来分析您的代码,就像它是在python 2下运行一样,然后您可以再次运行该工具,就像您的代码是在python 3下运行一样。通过运行两次这样的静态类型检查器,您可以发现您是否在一个版本的Python中错误地使用二进制数据类型。如果向代码中添加可选的类型提示,还可以显式地说明API是使用文本数据还是二进制数据,这有助于确保在两个版本的python中,一切都能正常工作。