将你的应用迁移到 Python 3 的三个步骤
Python

> 
> Python 2 气数将尽，是时候将你的项目从 Python 2 迁移到 Python 3 了。
> 
> 
> 


![](/data/attachment/album/201912/20/095224r0kp79s0cnc0z77p.jpg)


Python 2.x 很快就要[失去官方支持](/article-11629-1.html)了，尽管如此，从 Python 2 迁移到 Python 3 却并没有想象中那么难。我在上周用了一个晚上的时间将一个 3D 渲染器的前端代码及其对应的 [PySide](https://pypi.org/project/PySide/) 迁移到 Python 3，回想起来，尽管在迁移过程中无可避免地会遇到一些牵一发而动全身的修改，但整个过程相比起痛苦的重构来说简直是出奇地简单。


每个人都别无选择地有各种必须迁移的原因：或许是觉得已经拖延太久了，或许是依赖了某个在 Python 2 下不再维护的模块。但如果你仅仅是想通过做一些事情来对开源做贡献，那么把一个 Python 2 应用迁移到 Python 3 就是一个简单而又有意义的做法。


无论你从 Python 2 迁移到 Python 3 的原因是什么，这都是一项重要的任务。按照以下三个步骤，可以让你把任务完成得更加清晰。


### 1、使用 2to3


从几年前开始，Python 在你或许还不知道的情况下就已经自带了一个名叫 [2to3](https://docs.python.org/3.1/library/2to3.html) 的脚本，它可以帮助你实现大部分代码从 Python 2 到 Python 3 的自动转换。


下面是一段使用 Python 2.6 编写的代码：



```
#!/usr/bin/env python
# -*- coding: utf-8 -*-
mystring = u'abcdé'
print ord(mystring[-1])
```

对其执行 2to3 脚本：



```
$ 2to3 example.py
RefactoringTool: Refactored example.py
--- example.py     (original)
+++ example.py     (refactored)
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-mystring = u'abcdé'
-print ord(mystring[-1])
+mystring = 'abcdé'
+print(ord(mystring[-1]))
RefactoringTool: Files that need to be modified:
RefactoringTool: example.py
```

在默认情况下，`2to3` 只会对迁移到 Python 3 时必须作出修改的代码进行标示，在输出结果中显示的 Python 3 代码是直接可用的，但你可以在 2to3 加上 `-w` 或者 `--write` 参数，这样它就可以直接按照给出的方案修改你的 Python 2 代码文件了。



```
$ 2to3 -w example.py
[...]
RefactoringTool: Files that were modified:
RefactoringTool: example.py
```

`2to3` 脚本不仅仅对单个文件有效，你还可以把它用于一个目录下的所有 Python 文件，同时它也会递归地对所有子目录下的 Python 文件都生效。


### 2、使用 Pylint 或 Pyflakes


有一些不良的代码在 Python 2 下运行是没有异常的，在 Python 3 下运行则会或多或少报出错误，这种情况并不鲜见。因为这些不良代码无法通过语法转换来修复，所以 `2to3` 对它们没有效果，但一旦使用 Python 3 来运行就会产生报错。


要找出这种问题，你需要使用 [Pylint](https://opensource.com/article/19/10/python-pylint-introduction)、[Pyflakes](https://pypi.org/project/pyflakes/)（或 [flake8](https://opensource.com/article/19/5/python-flake8) 封装器）这类工具。其中我更喜欢 Pyflakes，它会忽略代码风格上的差异，在这一点上它和 Pylint 不同。尽管代码优美是 Python 的一大特点，但在代码迁移的层面上，“让代码功能保持一致”无疑比“让代码风格保持一致”重要得多。


以下是 Pyflakes 的输出样例：



```
$ pyflakes example/maths
example/maths/enum.py:19: undefined name 'cmp'
example/maths/enum.py:105: local variable 'e' is assigned to but never used
example/maths/enum.py:109: undefined name 'basestring'
example/maths/enum.py:208: undefined name 'EnumValueCompareError'
example/maths/enum.py:208: local variable 'e' is assigned to but never used
```

上面这些由 Pyflakes 输出的内容清晰地给出了代码中需要修改的问题。相比之下，Pylint 会输出多达 143 行的内容，而且多数是诸如代码缩进这样无关紧要的问题。


值得注意的是第 19 行这个容易产生误导的错误。从输出来看你可能会以为 `cmp` 是一个在使用前未定义的变量，实际上 `cmp` 是 Python 2 的一个内置函数，而它在 Python 3 中被移除了。而且这段代码被放在了 `try` 语句块中，除非认真检查这段代码的输出值，否则这个问题很容易被忽略掉。



```
    try:
        result = cmp(self.index, other.index)
    except:
        result = 42
       
    return result
```

在代码迁移过程中，你会发现很多原本在 Python 2 中能正常运行的函数都发生了变化，甚至直接在 Python 3 中被移除了。例如 PySide 的绑定方式发生了变化、`importlib` 取代了 `imp` 等等。这样的问题只能见到一个解决一个，而涉及到的功能需要重构还是直接放弃，则需要你自己权衡。但目前来说，大多数问题都是已知的，并且有[完善的文档记录](https://docs.python.org/3.0/whatsnew/3.0.html)。所以难的不是修复问题，而是找到问题，从这个角度来说，使用 Pyflake 是很有必要的。


### 3、修复被破坏的 Python 2 代码


尽管 `2to3` 脚本能够帮助你把代码修改成兼容 Python 3 的形式，但对于一个完整的代码库，它就显得有点无能为力了，因为一些老旧的代码在 Python 3 中可能需要不同的结构来表示。在这样的情况下，只能人工进行修改。


例如以下代码在 Python 2.6 中可以正常运行：



```
class CLOCK_SPEED:
        TICKS_PER_SECOND = 16
        TICK_RATES = [int(i * TICKS_PER_SECOND)
                      for i in (0.5, 1, 2, 3, 4, 6, 8, 11, 20)]

class FPS:
        STATS_UPDATE_FREQUENCY = CLOCK_SPEED.TICKS_PER_SECOND
```

类似 `2to3` 和 Pyflakes 这些自动化工具并不能发现其中的问题，但如果上述代码使用 Python 3 来运行，解释器会认为 `CLOCK_SPEED.TICKS_PER_SECOND` 是未被明确定义的。因此就需要把代码改成面向对象的结构：



```
class CLOCK_SPEED:
        def TICKS_PER_SECOND():
                TICKS_PER_SECOND = 16
                TICK_RATES = [int(i * TICKS_PER_SECOND)
                        for i in (0.5, 1, 2, 3, 4, 6, 8, 11, 20)]
                return TICKS_PER_SECOND

class FPS:
        STATS_UPDATE_FREQUENCY = CLOCK_SPEED.TICKS_PER_SECOND()
```

你也许会认为如果把 `TICKS_PER_SECOND()` 改写为一个构造函数（用 `__init__` 函数设置默认值）能让代码看起来更加简洁，但这样就需要把这个方法的调用形式从 `CLOCK_SPEED.TICKS_PER_SECOND()` 改为 `CLOCK_SPEED()` 了，这样的改动或多或少会对整个库造成一些未知的影响。如果你对整个代码库的结构烂熟于心，那么你确实可以随心所欲地作出这样的修改。但我通常认为，只要我做出了修改，都可能会影响到其它代码中的至少三处地方，因此我更倾向于不使代码的结构发生改变。


### 坚持信念


如果你正在尝试将一个大项目从 Python 2 迁移到 Python 3，也许你会觉得这是一个漫长的过程。你可能会费尽心思也找不到一条有用的报错信息，这种情况下甚至会有将代码推倒重建的冲动。但从另一个角度想，代码原本在 Python 2 中就可以运行，要让它能在 Python 3 中继续运行，你需要做的只是对它稍加转换而已。


但只要你完成了迁移，你就得到了这个模块或者整个应用程序的 Python 3 版本，外加 Python 官方的长期支持。




---


via: <https://opensource.com/article/19/12/update-apps-python-3>


作者：[Seth Kenlon](https://opensource.com/users/seth) 选题：[lujun9972](https://github.com/lujun9972) 译者：[HankChow](https://github.com/HankChow) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OK
The 2.x series of Python is [officially over as of January 2020](https://opensource.com/article/19/11/end-of-life-python-2), and [unsupported after April 2020](https://www.python.org/psf/press-release/pr20191220/), but converting code to Python 3 is easier than you think. Over the weekend, I spent an evening converting the frontend code of a 3D renderer (and its corresponding [Python for Qt/PySide](https://pypi.org/project/PySide/) version) to Python 3, and it was surprisingly simple in retrospect, although it seemed relatively hopeless during the refactoring process. The conversion process can seem a little like a labyrinth, with every change you make revealing a dozen more changes you need to make.

You may or may not *want* to do the conversion, but—whether it's because you procrastinated too long or you rely on a module that won't be maintained unless you convert—sometimes you just don't have a choice. And if you're looking for an easy task to start your contribution to open source, converting a Python 2 app to Python 3 is a great way to make an easy but meaningful impression.

Whatever your reason for refactoring Python 2 code into Python 3, it's an important job. Here are three steps to approach the task with clarity.

## 1. Run 2to3

For the past several years, Python has shipped with a script called [ 2to3](https://docs.python.org/3.1/library/2to3.html), which does the bulk of the conversion from Python 2 to Python 3 for you. Automatically. And you already have it installed (whether you realize it or not).

Here's a short snippet of code written in Python 2.6:

```
#!/usr/bin/env python
# -*- coding: utf-8 -*-
mystring = u'abcdé'
print ord(mystring[-1])
```


Run the **2to3** script:

```
$ 2to3 example.py
RefactoringTool: Refactored example.py
--- example.py (original)
+++ example.py (refactored)
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-mystring = u'abcdé'
-print ord(mystring[-1])
+mystring = 'abcdé'
+print(ord(mystring[-1]))
RefactoringTool: Files that need to be modified:
RefactoringTool: example.py
```


By default, **2to3** prints only the changes required to bring old Python code up to Python 3 standards. The output is a usable patch you can use to change your file, but it's easier to just let Python do that for you, using the **--write** (or **-w**) option:

```
$ 2to3 -w example.py
[...]
RefactoringTool: Files that were modified:
RefactoringTool: example.py
```


The **2to3** script doesn't work on just a single file. You can run it on an entire directory of Python files, with or without the **--write** option, to process all ***.py** files in the directory and its subdirectories.

## 2. Use Pylint or Pyflakes

It's not uncommon to discover code quirks that ran without issue in Python 2 but don't work so well in Python 3. Because these quirks can't be fixed by converting syntax, they get past **2to3** unchanged, but they fail once you try to run the code.

To detect such issues, you can use an application like [Pylint](https://opensource.com/article/19/10/python-pylint-introduction) or a tool like [Pyflakes](https://pypi.org/project/pyflakes/) (or the [flake8](https://opensource.com/article/19/5/python-flake8) wrapper). I prefer Pyflakes because, unlike Pylint, it ignores deviations in the *style* of your code. While the "prettiness" of Python is often praised as one of its strong points, when porting someone else's code from 2 to 3, treating style and function as two separate bugs is a matter of prioritization.

Here's example output from Pyflakes:

```
$ pyflakes example/maths
example/maths/enum.py:19: undefined name 'cmp'
example/maths/enum.py:105: local variable 'e' is assigned to but never used
example/maths/enum.py:109: undefined name 'basestring'
example/maths/enum.py:208: undefined name 'EnumValueCompareError'
example/maths/enum.py:208: local variable 'e' is assigned to but never used
```


This output (compared to 143 lines from Pylint, most of which were complaints about indentation) clearly displays the problems in the code that you should repair.

The most interesting error here is the first one, on line 19. It's a little misleading because you might think that **cmp** is a variable that was never defined, but **cmp** is really a function from Python 2 that doesn't exist in Python 3. It's wrapped in a **try** statement, so the issue could easily go unnoticed until it becomes obvious that the **try** result is not getting produced.

```
try:
result = cmp(self.index, other.index)
except:
result = 42
return result
```


There are countless examples of functions that no longer exist or that have changed between when an application was maintained as a Python 2 codebase and when you decide to port it. PySide(2) bindings have changed, Python functions have disappeared or been transformed (**imp** to **importlib**, for example), and so on. Fix them one by one as you encounter them. Even though it's up to you to reimplement or replace those missing functions, by now, most of these issues are known and [well-documented](https://docs.python.org/3.0/whatsnew/3.0.html). The real challenge is more about catching the errors than fixing them, so use Pyflakes or a similar tool.

## 3. Repair broken Python 2 code

The **2to3** script gets your code Python 3 compliant, but it only knows about differences between Python 2 and 3. It generally can't make adjustments to account for changes in libraries that worked one way back in 2010 but have had major revisions since then. You must update that code manually.

For instance, this code apparently worked back in the days of Python 2.6:

```
class CLOCK_SPEED:
TICKS_PER_SECOND = 16
TICK_RATES = [int(i * TICKS_PER_SECOND)
for i in (0.5, 1, 2, 3, 4, 6, 8, 11, 20)]
class FPS:
STATS_UPDATE_FREQUENCY = CLOCK_SPEED.TICKS_PER_SECOND
```


Automated tools like **2to3** and **Pyflakes** don't detect the problem, but Python 3 doesn't see **GAME_SPEED.TICKS_PER_SECOND** as a valid statement because the function being called was never explicitly declared. Adjusting the code is a simple exercise in object-oriented programming:

```
class CLOCK_SPEED:
def TICKS_PER_SECOND():
TICKS_PER_SECOND = 16
TICK_RATES = [int(i * TICKS_PER_SECOND)
for i in (0.5, 1, 2, 3, 4, 6, 8, 11, 20)]
return TICKS_PER_SECOND
class FPS:
STATS_UPDATE_FREQUENCY = CLOCK_SPEED.TICKS_PER_SECOND()
```


You may be inclined to make it cleaner still by replacing the **TICKS_PER_SECOND** function with a constructor (an **__init__** function to set default values), but that would change the required call from **CLOCK_SPEED.TICKS_PER_SECOND()** to just **CLOCK_SPEED()**, which may or may not have ramifications elsewhere in the codebase. If you know the code well, then you can use your better judgment about how


much alteration is *required* and how much would just be pleasant, but generally, I prefer to assume every change I make inevitably demands at least three changes to every other file in the project, so I try to work within its existing structure.

## Don't stop believing

If you're porting a very large project, it sometimes starts to feel like there's no end in sight. It can seem like forever before you see a useful error message that's *not* about a Python 2 quirk that slipped past the scripts and linters, and once you get to that point, you'll start to suspect it would be easier to just start from scratch. The bright side is that you (presumably) know that the codebase you're porting works (or worked) in Python 2, and once you make your adjustments, it will work again in Python 3; it's just a matter of conversion.

Once you've done the legwork, you'll have a Python 3 module or application, and regular maintenance (and those style changes to make Pylint happy) can begin anew!

## Comments are closed.