2、环境迁移实战:从 Python 2 到 Python 3 的迁移策略、工具使用与常见兼容性问题

说实话,Python 2 到 Python 3 的迁移,是我职业生涯里碰到过最「磨人」的一次升级。不是技术多难,而是项目大了以后,牵一发动全身。我记得 2019 年底,我们团队接手一个维护了 6 年的数据分析平台,代码全是 Python 2.7 写的。官方停止支持的消息一出来,老板就催着迁移。嗯,那段时间我几乎每天都在跟 UnicodeDecodeError 和 print 括号较劲。

这一章,我就把当时踩过的坑、用顺手的工具,还有总结出来的迁移策略,一次性讲清楚。你想想看,如果能把迁移这件事拆成几个明确的步骤,其实没那么可怕。

2.1 迁移的整体策略:别想着一步到位

我个人的习惯是,先把代码库「摸个底」。别上来就开干,先搞清楚你的项目有多大、依赖了哪些第三方库、测试覆盖率怎么样。说白了,迁移策略就三种:

  • 全部重写:适合小脚本或新项目,但大项目不推荐。
  • 逐步迁移:用 six 或 future 库写兼容层,让代码同时在 2 和 3 上跑。我最推荐这种方式。
  • 一次性转换:用 2to3 工具全量转换,然后手动修 bug。适合测试覆盖率高的小团队。
我的建议: 如果你维护的是线上服务,选「逐步迁移」。先让代码兼容 Python 2.7 和 3.x,跑一段时间稳定了,再彻底扔掉 Python 2 的支持。我在项目中用过这个方案,风险最小。

2.2 核心工具:2to3 与 six 库

工具选对了,能省一半力气。下面这两个是我最常用的。

2.2.1 2to3:自动化转换的「手术刀」

2to3 是 Python 官方提供的转换工具。它能自动把 Python 2 的语法转成 Python 3 的语法。比如 print 加括号、except Exception, e 改成 except Exception as e 等等。

基本用法很简单:

# 先看看会改哪些文件(不实际修改)
2to3 -n -W your_project/

# 实际执行转换
2to3 -n -W --no-diffs your_project/

但我要提醒你一句:2to3 不是万能的。它只能处理语法层面的变化,像 str 和 bytes 的语义变化、dict.keys() 返回视图对象这些,它改不了。我曾经用 2to3 转完一个模块,跑测试直接挂了 30 多个用例,全是编码问题。

注意: 2to3 会修改源文件。建议先在 Git 分支上操作,或者加 -n 参数只输出 diff,确认没问题再写回。

2.2.2 six 库:写兼容代码的「瑞士军刀」

six 是一个轻量级的兼容库,名字来源于 2×3。它让你在同一个代码文件里同时支持 Python 2 和 Python 3。我个人觉得,对于不想分叉代码库的项目,six 是首选。

举个例子,处理字符串类型:

import six

# 判断字符串类型
if isinstance(value, six.string_types):
    print("这是一个字符串")

# 统一处理文本和二进制
text = six.ensure_text(b'hello')   # 返回 str
binary = six.ensure_binary('hello') # 返回 bytes

还有 six.moves 模块,专门处理那些被移动位置的库:

from six.moves import urllib
from six.moves import range as xrange  # Python 3 没有 xrange

# 用 urllib.request 替代 urllib2
response = urllib.request.urlopen('http://example.com')
核心思路: 用 six 写一次代码,跑在两个版本上。等完全切换到 Python 3 后,再通过 six 的文档找到对应原生写法,一次性替换掉。

2.3 常见兼容性问题与避坑指南

下面这几个问题,是我在迁移过程中反复遇到的。每一个都曾经让我 debug 到深夜。

2.3.1 字符串与字节串的「世纪之战」

Python 2 里 str 是字节串,unicode 是文本。Python 3 里 str 是文本,bytes 是字节串。这个变化是最大的坑。

我曾经遇到过一个问题:一个配置文件读取模块,在 Python 2 下跑得好好的,迁移到 Python 3 后,读出来的字符串全是 b'...' 形式,导致后续所有字符串操作都报错。

解决方案:

# 明确区分输入来源
def read_config(path):
    # 以文本模式读取
    with open(path, 'r', encoding='utf-8') as f:
        return f.read()

# 处理网络数据时,明确编码
data = response.read()
text = data.decode('utf-8')

2.3.2 print 语句与函数

这个最简单,但也最容易漏。Python 2 的 print "hello" 在 Python 3 里必须写成 print("hello")。2to3 能自动处理大部分,但如果你在字符串格式化里用了 print,还是要手动检查。

# Python 2 写法
print "结果:", value

# Python 3 写法
print("结果:", value)

# 兼容写法(用 six)
from __future__ import print_function
print("结果:", value)

2.3.3 整数除法行为变化

Python 2 里 3 / 2 = 1(整数除法),Python 3 里 3 / 2 = 1.5。这个变化很容易导致计算逻辑出错。

我在迁移一个金融计算模块时,就因为除法问题,导致利息计算结果全部偏小。排查了整整一天。

# 兼容写法
from __future__ import division

# 或者显式使用 //
result = 3 // 2  # 整数除法,结果为 1
result = 3 / 2   # 浮点除法,结果为 1.5

2.3.4 异常语法变化

Python 2 支持 except Exception, e:,Python 3 只支持 except Exception as e:。2to3 能改,但如果你手写兼容代码,要注意。

# 兼容写法
import sys

try:
    risky_operation()
except Exception as e:
    # Python 2 和 3 都支持
    print("Error:", e)
    # 获取 traceback
    import traceback
    traceback.print_exc()

2.3.5 字典视图与迭代器

Python 2 里 dict.keys() 返回列表,Python 3 返回视图对象。视图对象不支持索引操作,也不能直接修改。

# 兼容写法
d = {'a': 1, 'b': 2}

# 需要列表时,显式转换
keys_list = list(d.keys())

# 需要迭代时,直接用
for key in d:
    print(key)

# 或者用 six
from six import iterkeys
for key in iterkeys(d):
    print(key)

2.4 迁移流程总结

下面这张图,是我自己总结的迁移流程。每次接手新项目,我都按这个步骤来。

Python 2 → Python 3 迁移流程 1. 代码摸底 2. 工具扫描 3. 手动修复 4. 兼容测试 5. 上线验证 关键:每一步都要跑测试,不要跳过「兼容测试」直接上线

2.5 迁移检查清单

最后,我整理了一份检查清单。每次迁移完一个模块,我都会对照着过一遍。

检查项 说明 工具/方法
字符串编码 所有 str 和 bytes 使用是否正确 手动审查 + six.string_types
print 语法 是否全部改为函数调用 2to3 自动转换
整数除法 是否引入 from __future__ import division 手动检查 + 测试用例
异常语法 except Exception, e 是否改为 as e 2to3 自动转换
字典视图 dict.keys() 等是否依赖列表特性 手动审查 + six.iterkeys
第三方库 所有依赖是否支持 Python 3 pip list + 官方文档
测试覆盖率 迁移后测试是否全部通过 pytest / unittest
一个小技巧: 我习惯在迁移前先跑一次完整的测试套件,记录通过率。迁移后再跑一次,对比差异。这样能快速定位哪些模块出了问题。

好了,这一章的内容就到这里。迁移这件事,说白了就是「细心 + 工具 + 测试」。工具帮你省时间,细心帮你避坑,测试帮你兜底。你只要按流程走,不会出大问题。