将 Python 2 代码移植到 Python 3 — Python 文档
将 Python 2 代码移植到 Python 3
- 作者
- 布雷特·坎农
抽象的
由于 Python 3 是 Python 的未来,而 Python 2 仍在积极使用中,因此最好让您的项目可用于 Python 的两个主要版本。 本指南旨在帮助您弄清楚如何最好地同时支持 Python 2 和 3。
如果您希望移植扩展模块而不是纯 Python 代码,请参阅 将扩展模块移植到 Python 3 。
如果您想阅读一位核心 Python 开发人员对 Python 3 出现的原因的看法,您可以阅读 Nick Coghlan 的 Python 3 问答或布雷特·坎农的为什么存在 Python 3 .
如需移植方面的帮助,您可以通过电子邮件将问题发送至 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( 毒物可以帮助测试多个版本的 Python;
pip install tox
) - 考虑使用可选的静态类型检查来确保您的类型用法在 Python 2 和 3 中都适用(例如 用我的在 Python 2 和 Python 3 下检查您的输入)。
细节
同时支持 Python 2 和 3 的一个关键点是您可以开始今天 ! 即使您的依赖项尚不支持 Python 3,但这并不意味着您不能将代码 now 现代化以支持 Python 3。 即使在 Python 2 代码中,支持 Python 3 所需的大多数更改也可以使用更新的实践使代码更清晰。
另一个关键点是,将 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 代码。 在最坏的情况下,您将不得不在某些情况下使用函数而不是方法,或者必须导入函数而不是使用内置函数,否则整个转换对您来说应该不会感到陌生。
但是您的目标应该是仅支持 Python 2.7。 Python 2.6 不再受到免费支持,因此不会收到错误修正。 这意味着 you 将不得不解决您在 Python 2.6 中遇到的任何问题。 本 HOWTO 中还提到了一些不支持 Python 2.6 的工具(例如,Pylint),随着时间的推移,这将变得越来越普遍。 如果您只支持您必须支持的 Python 版本,那对您来说会更容易。
确保在 setup.py 文件中指定正确的版本支持
在您的 setup.py
文件中,您应该有正确的 trove 分类器 指定您支持的 Python 版本。 由于您的项目不支持 Python 3,您至少应该指定 Programming Language :: Python :: 2 :: Only
。 理想情况下,您还应该指定您支持的每个主要/次要 Python 版本,例如 Programming Language :: Python :: 2.7
。
具有良好的测试覆盖率
一旦您的代码支持您想要的最旧版本的 Python 2,您将需要确保您的测试套件具有良好的覆盖率。 一个好的经验法则是,如果您想对您的测试套件有足够的信心,那么在工具重写您的代码后出现的任何故障都是工具中的实际错误,而不是您的代码中的错误。 如果您想要一个数字作为目标,请尝试超过 80% coverage(如果您发现很难超过 90% coverage,请不要难过)。 如果您还没有测量测试覆盖率的工具,那么推荐使用 coverage.py。
了解 Python 2 和 3 之间的差异
一旦您的代码经过良好测试,您就可以开始将代码移植到 Python 3 了! 但是要完全了解您的代码将如何更改以及您在编写代码时要注意什么,您需要了解 Python 3 相对于 Python 2 所做的更改。 通常,最好的两种方法是阅读每个 Python 3 版本的 “新增功能” 文档和 Porting to Python 3 书(在线免费)。 Python-Future 项目中还有一个方便的 备忘单 。
更新您的代码
一旦您觉得自己知道 Python 3 与 Python 2 的不同之处,就该更新您的代码了! 在自动移植代码时,您可以选择两种工具:Futurize 和 Modernize。 您选择哪种工具取决于您希望代码与 Python 3 的相似程度。 Futurize 尽最大努力使 Python 3 习语和实践存在于 Python 2 中,例如 从 Python 3 向后移植 bytes
类型,以便在 Python 的主要版本之间具有语义奇偶校验。 另一方面,Modernize 更为保守,它针对 Python 的 Python 2/3 子集,直接依靠 six 来帮助提供兼容性。 由于 Python 3 是未来,最好考虑 Futurize 开始适应 Python 3 引入的任何您还不习惯的新实践。
无论您选择哪种工具,它们都会更新您的代码以在 Python 3 下运行,同时与您开始使用的 Python 2 版本兼容。 根据您想要的保守程度,您可能希望首先在测试套件上运行该工具,然后目视检查差异以确保转换是准确的。 在转换测试套件并验证所有测试仍按预期通过后,您可以转换应用程序代码,知道任何失败的测试都是翻译失败。
不幸的是,这些工具无法自动化所有内容以使您的代码在 Python 3 下工作,因此您需要手动更新一些内容以获得完整的 Python 3 支持(这些步骤中哪些是必要的,因工具而异)。 阅读您选择使用的工具的文档以查看它默认修复的内容以及它可以选择做什么,以了解将为您修复(不)修复哪些内容以及您可能需要自行修复哪些内容(例如 在内置 open()
功能上使用 io.open()
功能在 Modernize 中默认关闭)。 幸运的是,只有几件事情需要注意,如果不注意,可能会被认为是难以调试的大问题。
分配
在 Python 3 中,5 / 2 == 2.5
而不是 2
; int
值之间的所有除法都会导致 float
。 这一变化实际上是自 2002 年发布的 Python 2.2 以来就计划好的。 从那时起,我们鼓励用户将 from __future__ import division
添加到使用 /
和 //
运算符的任何和所有文件中,或者使用 -Q
运行解释器旗帜。 如果您还没有这样做,那么您将需要检查您的代码并做两件事:
- 将
from __future__ import division
添加到您的文件中 - 根据需要更新任何除法运算符以使用
//
使用楼层除法或继续使用/
并期望浮点数
/
不能简单地自动转换为 //
的原因是,如果对象定义了 __truediv__
方法而不是 __floordiv__
,那么您的代码将开始失败(例如 一个用户定义的类,它使用 /
来表示某些操作,但不使用 //
来表示相同的事物或根本没有)。
文本与二进制数据
在 Python 2 中,您可以对文本和二进制数据使用 str
类型。 不幸的是,这种两种不同概念的融合可能会导致脆弱的代码,这些代码有时适用于任何一种数据,有时则不适用。 如果人们没有明确声明接受 str
的东西接受文本或二进制数据而不是一种特定类型,那么它也可能导致 API 混乱。 这使情况变得复杂,尤其是对于支持多种语言的任何人来说,因为 API 在声称支持文本数据时不会明确支持 unicode
。
为了使文本和二进制数据之间的区别更清晰、更明显,Python 3 做了互联网时代创建的大多数语言所做的事情,使文本和二进制数据成为不能盲目混合在一起的不同类型(Python 早于广泛访问互联网)。 对于仅处理文本或仅处理二进制数据的任何代码,这种分离不会造成问题。 但是对于必须同时处理这两者的代码,这确实意味着您现在可能必须关心与二进制数据相比何时使用文本,这就是为什么这不能完全自动化的原因。
首先,您需要决定哪些 API 采用文本,哪些采用二进制( 强烈 建议您不要设计可以同时采用两者的 API,因为难以保持代码正常工作;如前所述很难做好)。 在 Python 2 中,这意味着确保获取文本的 API 可以与 unicode
一起使用,而那些处理二进制数据的 API 可以与 Python 3 中的 bytes
类型(它是 str
在 Python 2 中充当 bytes
类型在 Python 2 中的别名)。 通常最大的问题是同时意识到 Python 2 和 3 中的哪些类型存在哪些方法(对于文本unicode
在 Python 2 和str
在 Python 3 中,对于二进制文件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,但使用表明它不如向所有添加 b
或 u
前缀有效明确的文字)
作为这种二分法的一部分,您还需要小心打开文件。 除非您一直在 Windows 上工作,否则您可能不会在打开二进制文件时总是费心添加 b
模式(例如,用于二进制读取的 rb
)。 在 Python 3 下,二进制文件和文本文件明显不同,并且互不兼容; 有关详细信息,请参阅 io 模块。 因此,您 必须 决定将文件用于二进制访问(允许读取和/或写入二进制数据)还是文本访问(允许读取和/或写入文本数据) . 您还应该使用 io.open() 来打开文件而不是内置的 open() 函数,因为 io 模块从 Python 2 到3 而内置的 open() 函数不是(在 Python 3 中它实际上是 io.open())。 不要为使用 codecs.open() 的过时做法而烦恼,因为这只是为了保持与 Python 2.5 的兼容性所必需的。
两者的构造函数str
和bytes
对于 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)
。
总结一下:
- 决定哪些 API 采用文本,哪些采用二进制数据
- 确保您的文本代码也适用于
unicode
,二进制数据的代码适用于 Python 2 中的bytes
(请参阅上表了解您不能用于每种类型的方法) - 用
b
前缀标记所有二进制文字,用u
前缀标记文本文字 - 尽快将二进制数据解码为文本,尽可能晚将文本编码为二进制数据
- 使用 io.open() 打开文件并确保在适当的时候指定
b
模式 - 索引二进制数据时要小心
使用特征检测代替版本检测
不可避免地,您将拥有必须根据正在运行的 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 2 与 Python 3 更兼容:
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
标志来整理您的代码,以便在您的代码开始偏离 Python 3 兼容性时收到警告。 这也可以防止您必须定期在代码上运行 Modernize 或 Futurize 来捕获兼容性回归。 这确实要求您只支持 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 支持。
您可能还想在 Python 3 解释器中使用 -bb
标志,以便在将字节与字符串或字节与 int(后者从 Python 3.5 开始可用)进行比较时触发异常。 默认情况下,类型不同的比较只返回 False
,但如果您在分离文本/二进制数据处理或字节索引时犯了错误,您将不会轻易发现错误。 当发生此类比较时,此标志将引发异常,从而更容易追踪错误。
主要是这样! 此时,您的代码库同时与 Python 2 和 3 兼容。 您的测试也将被设置,以便您不会意外地破坏 Python 2 或 3 的兼容性,无论您在开发时通常在哪个版本下运行测试。