python2.7 会在 2020 年停止维护, 很多第三方包也在去掉对 python2.7 的支持, 最近终于完成了内部代码向 python3 的迁移, 整个过程挺繁琐的, 记录一下.
总共需要迁移的代码大概有 50w 行(cloc 计算, 去注释空行), 包括业务代码 + ETL + data analysis... 前后花了3个月.
做之前确保已读过官方的 migration guide: https://docs.python.org/3/howto/pyporting.html
我的大致步骤:
celery 从 3.1.25 升级到 4.2.0, 问题挺多的.
CELERY_ACCEPT_CONENT
, 从4.0 开始默认只接受 json, 按需修改.
CELERY_RESULT_SERIALIZER
, 默认从 pickle 变成了 json , 务必不要使用pickle, python2/3 不兼容. 不过 json 不能序列化 binary, 有需要的话用 msgpack,或自己把task 结果 base64 encode.
4.0 开始如果用 redis 作为 broker, 当设置需要 task 的执行结果时, celery 内部会用 redis 的 pubsub 监听结果, 但 redis-py 的 pubsub 不是线程安全的, 在用 gevent 做 worker 时, pubsub 的 socket 会在多个greenlet 中被访问, 报错, workaround 是不设置 result_backend, 或给task 设置 ignore_result=True
.
在 py2.7 下 celery 4.X 的 AsyncResult 对象还有内存泄漏问题. 提了一个临时的 pull request:https://github.com/celery/celery/pull/4839 官方要在 4.3 里才会修复这个问题. 泄漏的原因是在一个有循环引用的 class 内部重载了__del__
函数, 在 python3.4 以前这种代码会内存泄漏.
最后把线上环境切换到 py3 的时候, 记得 celery 的 worker 节点要最后切换, 保证所有 producer 都是 py3 环境. 原因是 py2 入队的任务, 如果用的是 msgpack 作序列化, worker 是py3 的话, 解出来函数参数名都会变成 bytes, celery 内部对参数 unpack (**kwargs) 的时候就会报错.
这部分是最繁琐的, 有自动化工具可以辅助修改, 主要有 2to3 , future , modernize
2to3 是单向修改,生成的代码并不兼容 python2, 所以没有用.
future 这个工具尝试模拟 py3 里一些 class 的行为, 把对代码的修改限定在头部的 import 语句, 但实际试下来问题很大, 尤其是重载了 class 的一些 magic method, 会有各种问题, 不建议使用.
modernize 靠谱, 它会用 six 转写代码, 只发现一种情况改错了 isinstance(i, (int, float, long))
会被改成 isinstance(i,(int, float, int))
, 正确的写法是 isinstance(i, (six.integer_types, float))
.
关于 py2/3 的兼容写法,可以看这份文档 http://python-future.org/compatible_idioms.html , 忽略它里面 future 的写法, 自己用 six 转写.
下面补充一些文档里说的不够或 modernize 无法识别的
首先请确保自己 100% 理解 py2 里 str 和 unicode 的各种行为, 下面代码在 py2 下哪些成功? 成功结果是 unicode 还是 str, 失败的结果是 UnicodeEncodeError 还是 UnicodeDecodeError
'a' + u'啊'
u'a' + '啊'
'%s' % '啊'
'%s a' % u'啊'
u'%s 啊' % 'a'
u'.'.join(['a', '啊'])
'Hi {}'.format('啊')
'Hi {}'.format(u'啊')
u'Hi {}'.format('啊')
基本规则是:
+
, join
, replace
, "%s" % (...)
, 都视为字符串拼接,如果拼接的每部分都是 unicode, 结果就是 unicode. 每部分都是 str, 结果就是 str. 其中有一个是 unicode, 会将其他部分自动按 ascii 解码成 unicode.然后编写一个相对正确的 to_unicode
函数:
def to_unicode(v):
if isinstance(v, six.text_type):
return v
if isinstance(v, six.binary_type):
return v.decode('utf-8')
else:
# if v is int, will be converted to unicode string
return six.text_type(v)
对传入参数模糊不清,又确实需要 unicode 的地方使用.
base64 encode/decode 的结果在 py3 下是 bytes, 而且 encode 参数只接受 bytes.
hashlib 中的函数接受的参数都是 bytes.
写一个 to_bytes
函数:
def to_bytes(v):
if isinstance(v, bytes):
return v
if isinstance(v, six.text_type):
return v.encode('utf-8')
else:
# if v is int, will be converted to byte
v = six.text_type(v)
return v.encode('utf-8')
在 py3 下 bytes 拿去做 string format 不会报错,会得到 bytes 的 __str__
形式:
"%s" % b"abc" # "b'abc'"
"{}".format(b"abc") # "b'abc'"
比较容易出错的地方有 base64 decode/encode, redis client 的返回结果, 都是 bytes, 直接拿去作 string format 就有问题, 还不会报错(py2 下可能没问题).
标准库中的 json.dumps
, 如果传入的值中混了 bytes, 会序列化失败, 但用 simplejson.dumps
可以自动 decode.requests.post(json=value)
底层会检查是否安装了 simplejson
, 如果有就用simplejson, 否则用标准库.
iterkeys(), itervalues(), iteritems()
, 这种在 py3 里去除的, modernize 能自动修正
keys(), values(), items()
, 在 py3 下返回的是 view object, https://docs.python.org/3/library/stdtypes.html#dictionary-view-objects , 不能直接取 slice, 需要转成 list.
一种比较常见的错误写法:
d = {'a': 1}
for k in d.keys():
if k == 'a':
d.pop(k)
在 py3 下会报 RuntimeError: dictionary changed size during iteration
, 因为 .keys()
返回的是 dict key 的 view 对象, 遍历它实际在遍历 dict 自己 (类似遍历 list 的时候不能删除 item), 需要用 list(d.keys())
获得 key 的拷贝.
py2 里的除法默认是 floor division, py3 里是 true division, from __future__ import division
可以将py2 里的除法变成 py3 的行为.
In py2:
/2 # 0
In py3:
/2 # 0.5
如果需要 floor division, 显示用 //
. py3 里, operator.div
不存在了, 分成了 operator.truediv
和 operator.floordiv
modernize 默认不会修改用到除法的地方, 可以用 python-modernize -f classic_division .
, 让它帮我们找出代码中所有用到除法的地方, 人工修正语意, 比如一些计算图片宽高的代码, 除法结果一定需要整数, range(len(days) / 7)
这种代码就改成 //
.... 比较繁琐,只能人工 review 代码.
捕获的 exception 作用域在 py3 中只存在 except 的 block 里, 下面代码会访问不到 e
:
try:
/0
except Exception as e:
pass
print(e)
py2 里可以用 e.message
, py3 里没有了, 需要访问message, 直接用 str(e)
, 在py2/3 中都 work.
py2 里的 StringIO/cStringIO 没有了, 使用 io.BytesIO
和 io.StringIO
替换, 有个坑是和 csv模块一起工作的时候, py2 里要用io.BytesIO
, py3 里要用 io.String()
__iter__
In py2:
hasattr('abc', '__iter__') # False
hasattr(u'abc', '__iter__') # False
In py3:
hasattr('abc', '__iter__') # True
hasattr(b'abc', '__iter__') # True
不要用 __iter__
来区分 str 和 list/tuple, 直接用 isinstance .
In py2:
None > 0 # False
None > {} # False
None > () # False
...
{} > 1 # True
() > 1 # True
...
在 py3 中都直接会报 TypeError
, 这种错误其实还挺多的, 比如:
d = {'a': None}
if d.get('a') > 0:
pass
类似代码在 py2 中不会报错, 逻辑其实不对, 到 py3 下就暴露了.只能靠单元测试覆盖.
list.sort()
和 sorted()
函数不再接受 cmp
参数: https://docs.python.org/3/howto/sorting.html#the-old-way-using-the-cmp-parameter
兼容写法:
if six.PY2:
l.sort(cmp=cmp_func)
else:
from functools import cmp_to_key
l.sort(key=cmp_to_key(cmp_func))
python2 中的 hash 实现输出的是一个固定数值, python3 中的 hash 算法改了, 并且默认开启random seed, 每次进程重启都会被重置,
所以每次重启进程 hash 的输出结果都不一样. 使用 hashlib 中的稳定算法替代.
但有些 hash 的结果被持久化的存下来了怎么办? 可以实现一个 python3 的 c extension, 将python2 里的 fnv hash 算法 backport 到 python3: https://github.com/monsterxx03/legacyhash/blob/master/hash.c , 我只支持了 对 bytes, unicode, int 的 hash 计算.尽量不要用这种方式, 使用一个跨语言的稳定算法.
round 也有个小坑
In py2:
round(Decimal(1.1), 2) # -> float 1.1
In py3:
round(Decimal(1.1), 2) # -> Decimal(1.10)
.encode('utf-8')
的地方在 py3 下基本都有问题.