> * 原文地址：[Moving a large and old codebase to Python3](https://medium.com/@boxed/moving-a-large-and-old-codebase-to-python3-33a5a13f8c99)
> * 原文作者：[Anders Hovmöller](https://medium.com/@boxed?source=post_header_lockup)
> * 译文出自：[掘金翻译计划](https://github.com/xitu/gold-miner)
> * 本文永久链接：[https://github.com/xitu/gold-miner/blob/master/TODO/moving-a-large-and-old-codebase-to-python3.md](https://github.com/xitu/gold-miner/blob/master/TODO/moving-a-large-and-old-codebase-to-python3.md)
> * 译者：[Starriers](https://github.com/Starriers)
> * 校对者：[LynnShaw](https://github.com/LynnShaw),[steinliber](https://github.com/steinliber)

# 将一个旧的大型项目迁移到 Python 3

一年半前，我们就决定使用 Python 3 了。我们已经讨论了很长时间，现在是时候使用了！现在这个过程已经结束了，我们已经把生产环境的最后部署都迁移到了 Python 3

*   整个代码库大约有 240 k 行，不包括空行和注解。
*   这是一个基于 Web 的批处理任务系统。并且只有一个生产，部署环境。
*   代码库大约有 15 年的历史了。
*   虽然这是一个 Django 应用程序，但部分代码是先于 Django 公布之前写的。

关于修改 Python 3 的一些基本统计数据，是基于对 git 提交历史的粗略过滤产生的：

*   275 次提交
*   4080 次添加代码行
*   3432  次删除代码行

我发现有 109 个 jira 问题与这个项目相关。

### Py2 → six → py3

我们的理念一直是 py2 ￫py2/py3 ￫ py3  因为我们实在无法在实际生产中实现巨变，这种直觉也以令人惊讶的方式被证明是正确的。这意味着 [2 到 3](https://docs.python.org/2/library/2to3.html) 是不可能的，我认为这很常见。我们尝试过使用 2 to 3 来检测 Python 3 的兼容性问题，但很快这也被发现无法成立。基本上，这样的更改意味着在 Python 2 中的代码将被破坏。这样的改变不可行。

结论是使用 [six](http://six.readthedocs.io), 这是一个库，可以方便的构建一个在 Python 2 和 3 中都有效的代码库。

首当其冲的就是更新之前的依赖关系。这项工作需要立刻启动，因为之后会有更多的内容要更新。

### 现代化

[Python-modernize](https://python-modernize.readthedocs.io) 是我们选择进行迁移的工具。它是一个可以自动将 Py 2 代码库转换为可兼容 six 代码库的工具。我们首先引入一个测试，作为 CI 的一部分，来检查基于 modernize 的新代码是否已经准备好兼容 py3 了。这样做最大的效果的是让那些仍使用 Py 2 语法的人意识到新的处理方法，但这显然对将现有的 240 k 行代码转化到 six 作用不大。我们都有使用旧语法的坏习惯，这可以说是教学上的成功了，即使它对代码行的计数没有什么不同，它也被我们用于实验分支：

#### 实验分支

我新建了一个名为“Python 3 ”的分支，并做了以下操作：

*  在整个代码库上运行“python-modernize -n -w” 。它会在合适的地方修改代码。我经常做完这步后没有进行第一次提交就开始修复代码。这个错误步骤总是让我后悔，不止一次地迫使我重新开始做整件事情。即使这个阶段出错，最好还是先把它提交。因此将机器和人要做的事情分开显得尤为重要。
*  将所有用于函数体的依赖项导入到我们还没有修复的 py3。

这里的想法是“run ahead”，即看看如果我们没有使用过时的依赖项，我们会遇到什么问题。这个分支允许我在超级中断状态下可以非常快速地启动应用程序，至少可以运行一些单元测试。 这个分支有很大的不同，但我还是找到了把它应用在适当场景的方法。我使用优秀的 GitUp 来拆分、组合和提交。当一个提交看起来不错的时候，我会把它挑选到一个新的分支，然后发给代码审查。

没有人可以在这个分支上工作，因为它被不断地 rebase ，强制推送，滥用，但是它确实让项目向前推进了，而不用等待所有的依赖项被更新。我强烈推荐使用这种方法！

### 静态分析

我们添加了预提交钩子，所以如果您编辑了一个文件，就会收到建议将 Python 3 全部进行 modernize 更新的提示。

 `quote_plus`  的手动静态分析： 在处理 quote_plus 和 six 上有一些细微差别。最后，我们创建了自己的包装器，默认代码强制执行使用这个包装器，而不是使用标准库中的包装器，也不使用 six 中包装器。我们还静态检查了您从未给 quote_plus 发送过的字节。

我们修复了每个 diango 应用程序中所有的 python 3 问题，并在 CI 环境中使用一个白名单强制执行了这一点，所以您无法破坏一个曾经修复过的应用程序。

### 依赖

对于我们来说，解决依赖是最困难的部分。我们有很多依赖，所以花了很多时间，其中有两个依赖关系比较棘手：

*   splunk-lib. 我们依赖于 splunk，但是直到今天，他们仍然忽略所有[要求为客户端增加 py3 兼容性的愤怒的客户](https://github.com/splunk/splunk-sdk-python/issues/91)。我们团队中的一个人 [最后自己亲自动手来解决这个问题](https://github.com/tltx/splunk-sdk-python/)。Splunk 处理得真的很糟糕，它甚至把这个评论区的这个问题锁上了！这简直让人无法接受。
*   Cassandra. 我们的整个产品都在使用这个数据库，但是我们使用了一个有以前 API 模块的旧的驱动程序。对于我们来说，py3 的迁移过程中，这占据了很大的一部分，因此我们必须逐段重写所有的这些代码。

### 测试

我们的代码测试覆盖率大约有 65% 包括：单元、集成, 以及 UI 合并。 我们确实编写了更多的测试，但总体数量并没有发生太大的变化。考虑将覆盖率从 65% 提高到 66% ，意味着编写将近2000 行代码的测试，这一点也不奇怪。

我们必须跳过需要 Cassandra 的测试，同时修复这个依赖项。 我发明了一个有趣的小 hack 来使它发挥作用， [并写了这方面的文章](https://medium.com/@boxed/use-the-biggest-hammer-8425e4c71882).

### 代码更改

关于代码更改的说明，在如何将 py2 迁移到  six 的文档中并未提及 (也许是我们错过了)：

#### StringIO

我们在代码中大量使用 StringIO 。第一反应就是使用 six。但对于 StringIO 来说，这在几乎所有情况下 (但不是全部!)都被证明是错。基本上，我们必须非常仔细地考虑每一个我们使用 StringIO 的地方，并试图弄清楚我们是否应该用 io.StringIO, io.BytesIO 或者 six.StringIO 来替代它。这里犯错的表现通常为看起来像兼容 py3 的代码准备好了，在 py2 中可以正常运行，却实际上在 py3 中是失效的。

#### 从  __future__  中导入unicode_literals

这是一件好坏参半的事情。您可以通过将它添加到许多文件中来发现 bug，但是有时会在 py2 中引入 bug。 当日志突然在奇怪的地方，比如在字符串前写"u"时，它也会变得令人困扰。总的来说，这显然不是我所期望的效果。

#### str/bytes/unicode

这在很大程度上是您所期望的。我感到惊讶的是，在 py2 和 py3 中需要 str 。如果将来您使用 unicode_literals 导入，那么一些字符串需要从 `'foo'` 修改为 `str('foo')`。

#### six.moves

six.moves 的实现是一个非常奇怪的黑客行为，因此它不像它假装的普通 Python 模块那样运行。 我也不同意他们在 six.moves 中不包含 `mock` 的选择。我们必须使用他们的 API 来自己添加它，但这让我们很难开始工作，而且它要求我们将  `from mock import patch`  改为  `from six.moves import mock` 这也意味着 `patch` 现在变成了 `mock.patch` 。

#### CSV 的解析是不同的

如果你使用 csv 模块，你需要了解 csv342。在我看来，这应该是 six 的一部分。否则就意味着你没有意识到有问题。不过我们在许多地方都没有使用 csv342，所以您这里要做的工作可能会有所不同。

### 发布顺序

我们首先进行测试：

*   在 CI 中进行单元测试
*   在 CI 中进行集成和UI测试（不包括 Cassandra）
*   在 CI 中进行 Cassandra 测试 (这要晚于之前的步骤!)

接下来就是产品本身了。我们建立一台拥有能一次性切换到 py3 的能力的批处理机器，并且至关重要地是将其切换回来。当在 py3 上发生中断时，这一点就显得很重要了。这对我们来说是很好的，因为我们可以重新排队那些中断的任务，但是我们不能中断太多或者任何实际上是很关键的任务。我们使用 Sentry 来收集奔溃日志，所以很容易查看迁移到 py3 时遇到的所有问题，而且当我们修复了所有的问题时，我们需要再次迁移到 py3，直到我们得到一些问题，如此反复。

我们有如下环境：

*   Devtest: 开发人员在内部使用，所以大多数情况下，这只是用来测试数据库迁移。这个环境非常容易使用，所以这里不经常出问题。
*   IAT (内部验收测试)：用于验证更改，并在我们将更改推送到生产之前执行回归测试。
*   UAT (用户接受度测试): 客户可以访问的测试环境。用于需要准备客户系统的变更，或者让客户在上线前查看变更。这个环境在数据库迁移前几天才会迁移。
*   生产环境

我们按照以下顺序将 Python 3 发布到这些环境中：

*   Devtest 环境
*   短期 IAT 环境
*   长期 IAT 环境
*   一台短期的批处理生产机器
*   在工作期间使用的一台批处理生产机器
*   生产 SFTP
*   占一半生产的批处理机器
*   生产批次
*   生产 Web (在测试环境的长时间手动测试运行之后)
*   生产负载机器。这是批处理的一个特殊子集。它完成了我们产品中 CUP 和内存最多的部分。

负载机器暴露了与 Python 3 不兼容的客户数据配置，因此我们必须在 Python 2 中实现对这些情况的警告，并确保再次打开 Python 3 之前已经修复了它们。这花了几天时间，因为我们每天都会收到客户数据，所以每次都会有一个警告，这又让我们不得不再等一天。

### 生产中的惊喜

*   `'ß'.upper()` 在 py2 中是  `'ß'`  但是在 py3 中是  `'SS'` 。当产品的最后一部分迁移到 py3 时，最终导致了产品的崩溃！
*   在 py2 中对不同类型的对象进行比较和排序是有效的，但这隐藏了大量的 bug 。我们得到了一些令人讨厌的惊喜，因为这种行为以一些不明显的方式从堆栈中泄露出来，特别是在一些排序列表中存在  `None` 的时候。总的来说，这是一个胜利，因为我们发现了相当多的 bug 。 `None` 在 py2  的列表中排在第一位，这可能会让人感到惊讶（您可能会期望它被排序到接近于零的地方！), 现在我们只需要来处理它们。
*   `'{}'.format(b'asd')` 在  Python 2 中是 `'asd'` , 但是在 Python 3 中是 `"b'asd'"` 。在 Python 3 中，这里几乎任何其他行为都会更好： 输出为十六进制 ( 结果明显更不一样 ) ，旧的行为 (之前的代码运行)，或者抛出异常 (最好的行为！)。
*   `int('1_0')`  [在 py 3 中结果是 10 ](https://www.python.org/dev/peps/pep-0515/), 但是在 py2 中无效。这甚至在切换到 py3 之前就困扰了我们。因为这种错配导致了另一个在我们之前使用 py3 的团队给我们发送了我们认为无效而他们认为有效的有效值。我个人认为这个决定是错误的：非常严格的解析是更好的默认方式，我担心这将在未来几年会继续以微妙的方式困扰我们。

### 结论

最后，我们觉得在这件事上我们真的别无选择:  Python 2 的维护将在某个时刻停止，我们的依赖项仅限于 py3，最明显的就是 Django。但是，无论如何，我们还是想要进行这种转换，因为我们经常会被 bytes/Unicode 问题困扰，并且Python 3 仅仅是修复了 Python 2 中的许多小麻烦。这次迁移过程，我们已经在生产过程中发现了一些实际的漏洞/错误配置。我们也期待在任何地方都可以使用 f-string 和有序字典。


---

> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区，文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域，想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。
