Porting Python 2 Code to Python 3

作者:布雷特·加农

抽象

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

如果您要移植扩展模块而不是纯Python代码,请参阅Porting Extension Modules to Python 3

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

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

The Short Explanation

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

  1. 只担心支持Python 2.7
  2. 确保您有良好的测试覆盖率(coverage.py可以帮助; 安装 覆盖 t1>)
  3. 了解Python 2和3之间的区别
  4. Use Modernize or Futurize to update your code (pip install modernize or pip install future, respectively)
  5. 使用Pylint可帮助确保您不会在Python 3支持上回退(pip 安装 pylint t4 >
  6. 使用caniusepython3找出哪些依赖项阻止了您对Python 3的使用(pip install caniusepython3 t4 >
  7. 一旦你的依赖不再阻塞你,使用连续集成,以确保你保持兼容Python 2&3(tox可以帮助测试多个版本的Python; pip install tox

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

Details

同时支持Python 2和3的关键点是,您可以从今天开始即使你的依赖不支持Python 3,但这并不意味着你不能现代化你的代码现在支持Python 3。支持Python 3所需的大多数更改都会使用更新的代码,即使在Python 2中也是如此。

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

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

Drop support for Python 2.6 and older

虽然您可以让Python 2.5与Python 3一起使用,但是只使用Python 2.7会简单一些。如果删除Python 2.5不是一个选项,则six项目可以帮助您同时支持Python 2.5和3(pip 安装 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分类符,指定您支持的Python版本。由于您的项目不支持Python 3,因此您至少应该有编程 语言 :: Python :: 2 :: 仅指定理想情况下,您还应该指定您支持的每个主要/次要版本的Python,例如编程 语言 :: Python :: / t6>

Have good test coverage

一旦你的代码支持Python 2的最旧版本,你想要确保你的测试套件有良好的覆盖。一个好的经验法则是,如果你想在测试套件中有足够的信心,那么在工具重写代码之后出现的任何失败都是工具中的实际错误,而不是代码中的错误。如果你想要一个数字的目标,尝试超过80%的覆盖(如果你不能容易地超过90%不感觉不好)。如果您还没有测量测试覆盖率的工具,建议使用coverage.py

Learn the differences between Python 2 & 3

一旦你的代码经过测试,你就可以开始将你的代码移植到Python 3!但是要完全理解你的代码将如何改变,以及你想要在代码中注意些什么,你将需要了解Python 3在Python 2方面的变化。通常,这样做的两个最佳方式是阅读每个版本的Python 3和移植到Python 3(可在线免费)的“新增功能”Python-Future项目中还有一个方便的备忘表

Update your code

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

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

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

Division

在Python 3中,5 / 2 == 2.5 t0>而不是2int值之间的所有分隔结果为float这个变化实际上是从2002年发布的Python 2.2开始计划的。此后,我们鼓励用户将 __未来__ 导入 使用///运算符或使用-Q标志运行解释器的所有文件。如果你没有这样做,那么你需要通过你的代码,并做两件事:

  1. __未来__ 导入 分区
  2. 根据需要更新任何除法操作符号,以使用//使用floor除法或继续使用/并期望浮点

不能简单地将/翻译为//的原因是,如果对象定义了__truediv__方法而不是__floordiv__一个用户定义的类,它使用/来表示一些操作,但不是//)。

Text versus binary data

在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类型是一个别名)。Usually the biggest issue is realizing which methods exist for 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 in Python 2 and bytes in Python 3). 下表列出了跨Python 2和3的每种数据类型的唯一方法(例如,decode()方法可用于Python 2中的等价二进制数据类型或3,但它不能由文本数据类型在Python 2和3之间一致使用,因为Python 3中的str没有方法)。请注意,从Python 3.5起,__mod__方法已添加到字节类型。

文本数据二进制数据
解码
编码
格式
isdecimal
是数字

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

下一个问题是确保你知道代码中的字符串字面值是否代表文本或二进制数据。至少应该在表示二进制数据的任何字面值中添加b前缀。对于文本,您应使用 __ future __ import unicode_literals u前缀到文字字面值。

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

对于Python 2和3之间的相同参数,strbytes的构造函数具有不同的语义。在Python 2中将整数传递到bytes会给出整数的字符串表示形式:bytes(3) == '3'但是在Python 3中,的整数参数会给你一个字节对象,只要指定的整数,填满空字节:bytes(3) == b'\ x00 \ x00 \ x00'当将字节对象传递到str时,也需要类似的担心。在Python 2你只是得到字节对象回:str(b'3') == b'3' t0>。但是在Python 3中,你得到字节对象的字符串表示:str(b'3') == “b'3” / t3>

最后,二进制数据的索引需要仔细处理(切片而不是需要任何特殊处理)。In Python 2, b'123'[1] == b'2' while in Python 3 b'123'[1] == 50. 因为二进制数据只是二进制数的容器,Python 3返回您索引的字节的整数值。但是在Python 2中,因为字节 == str,索引返回一个单字节字节。six项目有一个名为six.indexbytes()的函数,它将返回一个整数,如Python 3:six.indexbytes(b'123' , 1)

总结:

  1. 决定哪些API接受文本,哪些接受二进制数据
  2. 确保您的代码与文本一起工作与unicode和二进制数据代码使用bytes在Python 2中(请参阅上表,您不能使用的方法每种类型)
  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[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[0] > 2:
    from importlib import abc
else:
    from importlib2 import abc

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

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

Prevent compatibility regressions

将代码完全翻译为与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兼容性时接收警告。这还可以避免您必须定期运行现代化Futurize以捕获兼容性回归。这需要你只支持Python 2.7和Python 3.4或更新版本,因为这是Pylint的最低Python版本支持。

Check which dependencies block your transition

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

该项目还提供了可以集成到测试套件中的代码,以便在没有依赖关系阻止您使用Python 3时,您将有一个失败的测试。这允许您避免手动检查依赖关系,并在您可以开始在Python 3上运行时迅速收到通知。

Update your setup.py file to denote Python 3 compatibility

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

Use continuous integration to stay compatible

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

您可能还希望使用Python 3解释器的-bb标志来触发异常,当您将字节数字符串或字节与int(后者从Python 3.5开始可用)比较时。默认情况下,类型不同的比较只返回False,但如果你在分离文本/二进制数据处理或字节索引时犯了错误,你不会轻易找到错误。当这些比较发生时,这个标志将引发异常,使得错误更容易跟踪。

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

Dropping Python 2 support completely

如果你能够完全删除对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中的分类以包含编程 语言 :: Python :: 3 :: 只有