最近我们从Python2.7, 全线升级到了Python3.5。

Python2 和 Python3 有啥区别啊?

在程序员的理想乡里, 程序语言理应只是个好用的工具。 然而在现实生活里, 程序语言甚至是程序语言的版本, 关系到工程的很多方面。 Python2和Python3就像是小黄车和摩拜一样, 在某几个大的特性上有区别, 但本质上都一样, 不过一些细微之处又有不同。

比如说 Python2 最坑的 Unicode. 写过 Python2 的人总会遇到 UnicodeDecodeErrorUnicodeEncodeError 这样的错。 在电话面试的时候, 我们回答候选人我们为什么会用 Python2, 也是把锅丢给谢老板:

嗯是这样的, 我们第一行代码到整个初期框架都是 CEO 选的。 他是美国回来的, 所以不知道中国要用中文, 也会遇到 Unicode 相关的问题。 于是他没想太多, 就选了 Python2.7.

真实原因还有很大一部分是因为当时一些库对 Python2 支持比较好

除了 Unicode 的区别, Python2 到 Python3 还有一些系统自带函数有变化。 比如 urllib.urlencode -> urllib.parse.urlencode, StringIO.StringIO -> io.BytesIO。 一般项目里面可以用six这个库来做兼容, 比如上面举的两个例子可以用six.moves.urllib.parse.urlencodesix.BytesIO来替换, Django也自带了一个sixdjango.utils.six. 不过我们是自己搭建环境, 所以其实不用考虑兼容性, 迁移工作大概的 Milestone 如下:

一些微小的工作

  1. 确保自己代码的兼容性。
  2. 确保第三方库的兼容性。
  3. 确保单元测试能过。
  4. 确保测试环境、集成测试能过。
  5. 正式环境切换 Python3!
  6. 宣布辉煌结果!

比如说讲些我们具体做的事情吧:

柳老板很早就想把谢老板的Python2换了。 所以大概在16年底的时候, 我们都有这个心理预期。 因为我们都是 PyCharm 用户, (JetBrains 家巨好用的 IDE) 所以都打开了 Python Compatibility Inspection。 然后写代码的时候注意不使用xrange(), dict.iteritems(), print , 而用range(), dict.items(), print()来代替。 对于Unicode一类的问题, 我们使用了__future__这个功能, 确保每个文件的头都是:

# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

# 第一行指定 encoding
# 第二行 absolute_import 指定优先从绝对路径 import
#   参见 PEP-0328: https://www.python.org/dev/peps/pep-0328
# 第二行 unicode_literals 指定字符串默认使用 unicode 类型
#   参见 PEP-3112: https://www.python.org/dev/peps/pep-3112

比如有个很蠢的py2to3的工具, 用它最好情况也只能把上面说到的“自己代码和系统依赖”给替换了。 具体的库替换、正确性验证还是得自己做。

后来我们又大概过了一遍用到的第三方库(requirements.txt)。 最大的依赖 Django 本身是2/3都兼容的, 然后像 celery/redis/requests 这种兼容性也妥妥的。 之前我们用的 AWS Python SDK - boto 不支持 Python3, 这个好说,对应的功能可以用boto3来替换。

比较麻烦的是当时用的微信库python-wechat-sdk。 这个主要是我们用到了微信的很多功能: 消息回复、用户管理、模板/图文、各种素材等。 本身代码量就不小, 而且之前的代码写的还比较屎, 亟需重构。 于是我们花了几个月来重构代码+换库, 最终换成了更科学更好用的wechatpy

代码上的准备工作做完以后 (虽然这是轻巧的一句话, 但是因为实际开发中, 不可能空出一段时间专门用于架构升级。 所以我们都是在各种业务需求中找夹缝做的, 大概用了半年。)

代码上的准备工作做完以后, 我们又用Python3跑了一下单元测试(UT)完善的自动测试,是平时的保障,是关键时刻的定心丸。 理论上 Python2 到 Python3, 不应当有任何外部表现差异, UT也不应当有错。 所以在修复了UT的错误以后, 我们就有信心切换 Python3 跑跑了。

这里还有个小差别, 就是我们之前的服务器用的是 Ubuntu 14 (Trusty), Ubuntu14 上默认的 Python3 是 Python3.4。 最新的 Ubuntu 版本是 Ubuntu 16 (Xenial), 上面默认的 Python3 是 Python3.5。 柳老板也心水了 Ubuntu16 很久了, 于是这次切换 Python3, 我们也顺便把服务器版本升到了 Ubuntu16。 不能print中文的feature也下掉了

然后就是测试环境切换 Python3, 生产环境切换 Python3 了。 这里也有个小插曲, 生产环境切换 Python3 的时候, 我们原本想着先只升级一部分Server(灰度), 但是 Celery 的 Python2 Producer抛出的任务, Python3 Consumer能接, 但是百分百完不成…

于是我们决定这还灰个蛋,不灰了! 找一天晚上点份奶茶和小龙虾, 全线切换Python3! 因为做好了前期准备, 所以我们大概只花了十分钟切换成了Python3~ 腰也不酸了, 颈椎也不疼了, 连美餐的饭也好像变得美味了起来。 现在的我司后台就是跑在Python3上的~

最后还有就是大家的开发环境也切换一下 Python3 啦, 以前写的 docstring 可以切换成 type hintings 啦之类的微小的工作了。

# Python 2 的时候这么写
def old_hint(messages, data=()):
    """
    :type messages: list
    :type data: list[dict]
    :rtype: str
    """
    pass


# Python 3 就可以这么写
def new_hint(messages: list, data: list(dict) = ()) -> str:
    pass

总结

从 Python 2 到 Python 3 的好处见仁见智, 对于不同的情况各有不同。 做成这么一件微小的事情, 感受比较深的就是:

  • 目标要清晰。 比如我们早早地就把 Python 3 提到了我们的 Scrum Board 上。 除了技术很清楚我们要做什么, 产品也会有意识地给我们升级架构留下排期。 目标清晰,大家步伐就会一致。

  • 要有推动者。 比如柳老板就充当了整件事情的推动者, 有序给大家分锅,具体哪个模块谁来重构,哪个库谁来换。 一些没人想干的麻烦/要背锅的事情他也都做了, 比如去修UT的兼容性之类的…

  • 互相信任真好。 后端升级架构, 其实各方面都会受影响, 比如上文提到的 Celery 那个坑就让我们紧急回退了版本, 或者类比一下游戏厂商每周例行的停服务器操作。 这时候产生的一些锅, 就被一线的队友背了, 他们也没多埋怨就帮我们擦屁股去了… ORZ 感恩!

不论如何,Python 3毕竟是趋势, Django的新版本不会支持Python 2, Celery的新版本也不会支持Python 2了。 我们也不能落后呀。

就像陈皓老师常说的一样:

技术债是还不完的,但我们也要一直还!

陈皓老师:不,我没说过这句话,你自己编的。