将Python 2代码移植到Python 3

author: Brett Cannon

摘要

Python 3是Python的未来,而Python 2仍然存在activeuse,最好让你的项目可用于两个主要版本的Python。本指南旨在帮助您弄清楚如何最好地支持Python 2&3同时

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

如果您想阅读一个核心Python开发人员对Python 3came的存在进行了解,你可以阅读Nick Coghlan的Python 3 Q&一个orBrett Cannon的为什么Python 3存在.

为了帮助移植,你可以通过电子邮件发送python-porting邮件列表withquestions.

简短说明

让你的项目成为单源Python 2/3兼容,基本步骤:

  1. 只担心支持Python 2.7
  2. 确保你有良好的测试覆盖率(coverage.py可以帮助; pip install coverage
  3. 了解Python 2&3
  4. 使用Futurize(或Modernize)更新你的代码(例如pip 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
  8. 考虑使用可选的静态类型检查来确保你的类型使用工具在Python 2&3(例如使用mypy来检查你在Python 2和Python 3下的输入).

详细信息

关于支持Python 2&3同时是你今天可以开始 !即使您的依赖项不支持Python 3,但这并不意味着您无法使代码现代化现在支持Python 3.大多数需要支持Python 3的更改导致更清晰的代码使用更新的实践,即使在Python 2代码中也是如此.

另一个关键点是,现代化Python 2代码以支持Python 3在很大程度上是自动化的。由于Python 3澄清了文本数据与二进制数据,您可能不得不做出一些API决策,现在大部分工作都是为您完成的,因此至少可以立即受益于自动更改.

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

对Python 2.6及更早版本的支持

虽然你可以让Python 2.5与Python 3一起工作,但是更容易如果你只需要使用Python 2.7。如果删除Python 2.5不是一个选项,那么这个六个项目可以帮助你支持Python 2.5&3同时(pip install six)。但是要意识到,这个HOWTO中列出的几乎所有项目都无法使用.

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

但你应该瞄准仅支持Python 2.7。Python 2.6不再受到免费支持,因此没有收到错误修正。这意味着必须解决你在Python 2.6中遇到的任何问题。在这个HOWTO中也提到了一些不支持Python 2.6(例如Pylint)的工具,随着时间的推移,这将变得越来越普遍。如果你只支持你必须支持的Python版本,那将简单易用.

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

In您的 setup.py文件你应该有适当的trove分类,指定你支持的Python版本。由于你的项目不支持Python 3,你至少应该指定Programming Language :: Python :: 2 :: Only。理想情况下,您还应该指定您支持的每个主要/次要版本的Python,例如Programming Language :: Python :: 2.7.

具有良好的测试覆盖率

一旦您的代码支持您想要的最旧版本的Python 2,我们希望确保您的测试套件具有良好的覆盖率。一个很好的规则是,如果你想在你的测试套件中有足够的信心,那么在工具重写你的代码之后出现的任何失败都是工具中的实际错误,而不是代码中的错误。如果你想要一个瞄准的数字,试着获得超过80%的覆盖率(如果你发现很难获得超过90%的覆盖率,那就不要感觉不好)。如果您还没有测量测试覆盖率的工具,那么建议使用覆洞.

了解Python 2&3

一旦您的代码经过充分测试,您就可以开始将代码移植到Python 3了!但是为了完全理解你的代码将如何改变并且在编写代码时你想要注意什么,你将需要了解Python 2在Python 2方面做出的改变。通常,这两个最好的方法是阅读“什么是新的”“每个版本的Python 3和thePorting to Python 3书籍(免费在线)的文档。Python-Future项目还有一个handycheat表.

更新你的代码

一旦你想知道Python 3与Python 2相比有什么不同,是时候更新你的了码!您可以在两个工具之间自动移植代码:Futurize和Modernize。您选择哪种工具将取决于您希望代码的Python 3程度。Futurize最大限度地使Python 3中存在Python 3习语和实践,例如,向后移动bytes从Python 3中键入,以便在Python的主要版本之间具有语义奇偶校验。另一方面,Modernize更加保守,并且是Python的2/3子集,直接依赖于6来帮助提供兼容性。由于Python 3是未来,最好考虑Futurize开始适应Python 3引入的任何你不习惯的新实践.

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

遗憾的是,这些工具无法使所有内容自动化以使您的代码在Python 3下工作,因此您需要手动更新以获得完整的Python 3支持(这些步骤中的哪些步骤在工具之间有所不同)。阅读您选择使用的工具的文档,以查看默认情况下修正的内容,以及可以选择知道哪些(不)为您修复的内容以及您可能必须自行修复的内容(例如使用io.open() overhe内置open()功能在Modernize中默认关闭)。幸运的是,只有一些需要注意的事项可以被认为是大问题,如果不注意的话可能很难调试.

Division

在Python 3中,5 / 2 == 2.5 并不是 2;在int之间的所有划分float。自从2002年发布的Python 2.2以来,实际上已经计划了这一变化。从那时起,用户被鼓励将from __future__ import division添加到使用///运算符或用-Q标志运行解释器。如果你没有这样做,那么你将需要完成你的代码并做两件事:

  1. from __future__ import division添加到你的文件
  2. 根据需要更新任何除法运算符//使用floordivision或继续使用/并期待一个浮点

/的原因不是简单地自动转换为//是ifan对象定义一个__truediv__方法而不是__floordiv__然后你的代码会开始失败(例如一个用户定义的类使用/ tosignify某些操作但不是//同样的事情或者根本没有).

文本与二进制数据

在Python 2中你可以使用str类型的文本和二进制数据。不幸的是两种不同概念的这种融合可能导致brittlecode有时适用于任何一种数据,有时不适用。如果人们没有明确说明接受了str接受文本或二进制数据而不是特定类型。特别是对于支持多种语言的人来说这种情况很复杂,因为API不会明确支持unicode当他们声称文本数据支持时

为了使文本和二进制数据之间的区别更加清晰和更加明确,Python 3完成了大多数语言在互联网时代所创造的内容,并使文本和二进制数据成为不同类型,不能盲目地混合在一起(Python早于广泛访问互联网)。对于任何只处理文本或仅处理二进制数据的代码,这种分离不构成问题。但是对于必须同时处理这两者的代码,它确实意味着你可能已经关心何时使用文本与二进制数据相比,这就是为什么这不能完全自动化.

首先,你需要决定哪些API采用文本和哪些采用二进制文件(它是高度建议你不要设计那些因为难以保持代码工作而需要的API;如前所述,这很难很好。在Python 2中,这意味着确保采用文本的API可以使用unicode那些使用二进制数据的人使用Python 3中的bytes类型(它是Python 2中str的一个子集,并且作为bytes输入Python 2)。通常最大的问题是实现哪些方法存在于Python 2&中的哪些类型。3同时(对于Python 2中的unicode文本和str在Python 3中,对于二进制文件,在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前缀添加到呈现二进制数据的anyliteral。对于文本,您应该在文本文字中添加一个u前缀。(有一个__future__导入强制所有未指定的文件都是Unicode,但用法表明它不如添加bu显式的所有文字的前缀)

作为这种二分法的一部分,你还需要小心打开文件。除非你一直在Windows上工作,否则你有可能不会总是加入b打开二进制文件时的模式(例如rb forbinary reading)。在Python 3下,二进制文件和文本文件显然是分明的,互不兼容;有关详细信息,请参阅io模块。因此,您必须决定文件是否将用于二进制访问(允许读取二进制数据和/或写入)或文本访问(允许读取和/或写入文本数据)。您还应该使用io.open()来打开文件而不是内置的open()函数,因为io模块从Python 2到3是一致的,而内置的open()功能不是(在Python 3中它实际上是io.open())。不要理会使用codecs.open()的过时练习,因为这只是为了保持与Python 2.5的兼容性.

strbytes对于Python 2和…之间的相同参数有不同的语义。3.将整数传递给bytes在Python 2中将为您提供整数的字符串表示形式:bytes(3) == "3".But在Python 3中,是bytes会给你一个字节对象,只要指定整数,填充空字节:bytes(3) == b"\x00\x00\x00"。将abytes对象传递给str。在Python 2中,您只需返回bytes对象:str(b"3") == b"3"。但是在Python 3中,你得到了thebytes对象的字符串表示:str(b"3") == "b"3"".

最后,二进制数据的索引需要小心处理(切片需要任何特殊处理)。在Python 2中,b"123"[1] == b"2"而在Python 3 b"123"[1] == 50。因为二进制数据只是二进制数的集合,所以Python 3会返回您索引的字节的整数值。但是在Python 2中因为bytes == str,indexing返回一项单项的字节。六个项目有一个功能名称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版本是否支持您所需的。如果由于某些原因不起作用,那么您应该对Python 2而不是Python 3进行版本检查。帮助解释一下,让我们来看看一个例子.

假装您需要访问自Python 3.3以来在Python标准库中可用的importlib功能,并且可以通过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

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

你也可以使用Pylint项目及其--py3k标记为lint您的代码在您的代码开始偏离Python 3兼容性时收到警告。这也可以防止您定期运行Modernize或Futurizeover您的代码以捕获兼容性回归。这确实要求你只支持Python 2.7和Python 3.4或更新版本,因为这是Pylint最小的Python版本支持.

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

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

该项目还提供了可以集成到测试套件中的代码,当您不再使用依赖关系阻止您使用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的支持.

你可能还想使用带有Python 3解释器的-bb标志来取消异常将字节与字符串或字节比较为int(后者从Python 3.5开始可用)。默认情况下,type-differentingcomparisons只返回False,但是如果你在分析文本/二进制数据处理或对字节索引时出错,你就不会轻易找到错误。当这些类型的比较发生时,这个标志会引发异常,使错误更容易追踪.

这主要是它!此时,您的代码库同时兼容了Python 2和3。您的测试也将被设置,以便您不会意外地破坏Python 2或3的兼容性,无论您在开发时通常运行测试的是哪个版本.

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

另一种帮助移植代码的方法是在代码上使用静态类型检查程序,如类型或pytype。这些工具可用于分析您的代码,因为它在Python 2下运行,然后您可以再次运行该工具,就好像您的代码在Python 3下运行一样。通过运行静态类型检查器两次,您可以发现是否是例如在一个Python版本中滥用二进制数据类型与另一个版本相比。如果在代码中添加可选类型提示,您还可以明确说明您的API是使用文本数据还是二进制数据,这有助于确保所有内容在Python的两个版本中都按预期运行.