最近我们从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.urlencode
six.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了
我们也不能落后呀。

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

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

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