gevent 迁移 Python 3 历程(一)

4

时隔一年多,gevent 的作者 Denis Bilenko 终于从创业的百忙之中,抽出时间打算 review 我在 2012 年的时候完成的 gevent 到 Python 3 的迁移工作

Skype 交谈中,Denis 问了几个问题,我发现有不少改动我已经忘记了当初写的原因了,这个案例教育我们,在做较大的修改的时候,尽量拆分成多个较小的提交,每个提交消息都尽量写清楚。^_^

因为过了一年多,gevent 的 master 上也改动了一些。我尝试了做 merge,发现结果不是很理想,再加上对当时修改又不是很满意了,于是乎,我选择了参考原来的改动,重新迁移一次。

插叙一段小插曲。其实在 Denis 联系我之前,我已经放弃他了——因为他实在是很久很久没有在 gevent 上活跃开发了,gevent 1.0 感觉也是憋了好久憋出来的。当时连蟒爹的 Tulip/asyncio 都眼瞅着要发布了,我就直接 fork 了个项目叫 gevent3,也就是 Python 3 版的、基于 asyncio 的 gevent,这个 gevent3 有机会再跟大家介绍。没想到刚 fork 完没做多久,就发生了故事开头写的事情。

言归正传。接下来我分段介绍我这几个月用业余时间几乎做完的第二次迁移工作,希望能对也在做向 Python 3 迁移工作的同学们有点帮助。

Denis 对迁移工作的要求是,用同一套代码,同时支持 Python 2.6, 2.7 和 3.3。除了 greenlet,最好不要再引入其他的依赖,甚至是 six——一个专注于解决用同一套代码支持不同 Python 版本问题的库。

软柿子

老虎吃天,无从下口。面对庞大的代码量,还得先捡软柿子捏。

比如说,Python 3 用 int 替代了 Python 2 的 long(和 int)。six 对这种情况有这么一段定义:

if PY3:
    integer_types = int,
else:
    integer_types = (int, long)

那么就可以简单地把所有能换成 integer_types 的地方都换成 integer_types,就像这样:

         def __init__(self, fileno, mode=None, close=True):
-            if not isinstance(fileno, (int, long)):
+            if not isinstance(fileno, integer_types):
                 raise TypeError('fileno must be int: %r' % fileno)

类似的软柿子还有:

if PY3:
    string_types = str,
    integer_types = int,
    text_type = str
    xrange = range
else:
    string_types = basestring,
    integer_types = (int, long)
    text_type = unicode
    xrange = xrange

这些替换都是很简单的,虽然只是一个开始,但是可以让接下来更复杂的工作有一个好的开始。请参考:https://pythonhosted.org/six/#constants

乾坤大挪移

Python 3 中,很多模块都改了名字,幸好多半接口并没有变化,所以为了同时能够支持 Python 2 和 3,可以简单地这么搞:

-from Queue import Full, Empty
+try:
+    from Queue import Full, Empty
+except ImportError:
+    from queue import Full, Empty

或者这样搞:

-import urllib2
+try:
+    import urllib2
+except ImportError:
+    from urllib import request as urllib2

还有一些其他不少重命名和重新规划,请参见:http://python3porting.com/stdlib.html

将来时

在 Python 3 中,print 变成了一个函数,这直接意味着这样的代码是语法错误的:

print "Hello, world!"

为了实现同一份代码同时支持 Python 2 和 3,这里我们可以用到一个叫做 __future__import——这个 import 可以在某些老版本的 Python 中添加一些新版本才有的语言特性。对于 print 来说,Python 3 风格的 print() 函数自 Python 2.6 起开始出现在 __future__ 中。谢天谢地,gevent 及时摒弃了 Python 2.5 的支持,我们可以统一使用 Python 3 风格的 print() 来写所有代码,而做到这一点只需要在所有用到 print 的 Python 文件开头写这么一句:

from __future__ import print_function

这样一来,这些文件就可以使用 Python 3 风格的 print() 函数了。最抓人的是,如果以后打算放弃 Python 2 支持的话,只需要(甚至不需要)把这一行 import 语句删掉就可以了。

要注意的是,from __future__ import ... 必须出现在所有非注释类代码的前面。

更多细节可以参考这里:http://docs.python.org/3/library/__future__.html

ps: 还有个小插曲。gevent 的代码里从 Python 代码树拷贝了一些测试文件,比如 greentests/2.6/test__xxxxxx.py,用以测试 monkey patch 上去的 gevent 代码的正确性。这些测试只会在指定 Python 版本下才会执行,所以我就没有给 2.6 和 2.7 的代码加 print_function。奇怪的事情发生了!2.6 和 2.7 的某个测试居然开始抱怨说,print "Hello, world!" 语法错误!没查原因我就默默地把 2.6 和 2.7 的测试文件都加上了 print_function……结果咯,Denis 不愿意,还是得去查原因。最后发现 greentest/monkey_test.py 那货是亲自 exec() 的 2.6 和 2.7 下面的某些测试代码,而我给 monkey_test.py 也加上了 print_function……所以说,有 exec() 调用存在的情况下,不要轻易相信 from __future__ import xxxx 只对当前文件起作用

异常处理

这是轻敌了的一部分。

一开始只是以为 Python 2 与 3 之间,异常处理的区别只在于语法——对于 Python 2.6 及以上版本只要这样改就好了:

 try:
     1/0
-except Exception, ex:
+except Exception as ex:
     pass

原来这里有大买卖。

同一段代码,最后加多一句:

try:
    1/0
except Exception as ex:
    pass
print(ex)

在 Python 2 上是这样的结果:

$ python2.7 extest.py 
integer division or modulo by zero

在 Python 3 上却是:

$ python3.3 extest.py 
Traceback (most recent call last):
  File "extest.py", line 5, in <module>
    print(e)
NameError: name 'ex' is not defined

原来,Python 3 去掉了 sys.exc_clear() 函数,把该行为嵌入了语言内部——也就是说,只要是出了 except 子句,Python 3 的解释器会自动清除异常状态,还会捎带手把异常变量引用(as 出来的那个)删掉。举个例子,还是同一段代码,稍微改一下:

import sys
try:
    1/0
except Exception as ex:
    pass
print(sys.exc_info())

在 Python 2 中执行:

$ python2.7 exclear.py 
(<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x104d1a0e0>)

但在 Python 3 中:

$ python3.3 exclear.py 
(None, None, None)

原来如此!基于这些知识,gevent 的某些代码就得改了——原先在 except 子句中经常有 exc_clear() 之后又做了一些事情,现在就得改成在 except 子句外面来做这些事情。比如 socket.recv() 就得这么改(片段):

     def recv(self, *args):
         while True:
             try:
                 return sock.recv(*args)
             except error as ex:
                 if ex.args[0] != EWOULDBLOCK or self.timeout == 0.0:
                     raise
-                sys.exc_clear()
-                self._wait(self._read_event)
+                if not PY3:
+                    sys.exc_clear()
+            self._wait(self._read_event)

我还是挺喜欢 Python 3 的这个改变的,因为这样一来异常处理就非常干净整洁了,except 子句画地为牢,有效地限制了无用信息的外漏;另外这种限制还可以在一定程度上建议人们,不要在 except 子句里面写太多的业务逻辑,把异常处理好,有啥事儿咱出来再说。

另外,Python 3 还在异常的栈跟踪信息上做了一些改进,比如这么一段代码:

try:
    1/0
except Exception as ex:
    None.non_exist()

就是在处理异常的时候,又弄坏了别的东西。Python 2 执行是这样的:

$ python2.7 tb.py 
Traceback (most recent call last):
  File "tb.py", line 4, in <module>
    None.non_exist()

Python 3:

$ python3.3 tb.py 
Traceback (most recent call last):
  File "tb.py", line 2, in <module>
    1/0
ZeroDivisionError: division by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "tb.py", line 4, in <module>
    None.non_exist()
AttributeError: 'NoneType' object has no attribute 'non_exist'

高端、大气、上档次有木有!Python 3 是这么实现这种异常链的:

  1. 当第一个异常对象产生时,traceback 信息会保存在该对象的 __traceback__ 属性中;
  2. 当第二个异常对象产生时,因为是在第一个异常的 except 子句中,所以第一个异常对象被保存在了第二个异常对象的 __context__ 属性中(当然第二个异常的 __traceback__ 属性同样保存了第二个异常的栈跟踪信息);
  3. 依次这样链下去,你就会得到一个异常链,你可以通过访问比如 ex.__context__.__context__.__traceback__ 来找到爷爷异常的栈跟踪信息。

这个美好的功能在这次 gevent 的迁移最后引来了好大一个麻烦,等讲到时再细说。

(未完待续,附项目地址:https://github.com/fantix/gevent

你可能感兴趣的

16 条评论
est · 2014年03月04日

兼容py2和py3的语法: print ("your shit here")

回复

fantix 作者 · 2014年03月04日

呵呵,感谢阅读!您提供的方案确实可以一定程度上兼容 2 和 3,但有些高级用法没有类似通用语法,请参考 http://python3porting.com/noconv.html 第二段。另外,使用 backport 还有一个好处就是可以严格避免 Python 2 语法的笔误。

回复

Phus_Lu · 2014年03月05日

Fantix 加油呀, 之前就用过你的 gevent3, 后来因为一些原因回退到 py2.7 了。现在很期待你和 Denis 的官方版!

回复

fantix 作者 · 2014年03月05日

多谢支持!昨天 Denis 刚重新修了一遍 socket/ssl(好多测试又挂了 - -#),估计距离 1.1 不远啦。

回复

依云 · 2014年03月05日

赞!我从一开始就以 Python 3 为主了,所以很多差异的细节都还不太清楚呢。

SF 的博客没有 RSS 支持好遗憾啊 :-(

回复

fantix 作者 · 2014年03月05日

谢赞!能从 Python 3 为主开始真是很幸福啊,我是从 Python 2.3 生淌过来的呵呵呵。您说的是这个么:http://blog.segmentfault.com/fantix/feeds/

回复

依云 · 2014年03月05日

啊,原来有 RSS!为什么页面里只写了「所有问题」的 RSS 链接呢…………

其实我当初是离线看 Python 2.6 文档的,然后那文档里经常说 Py3K 里怎么样怎么样,也确实少了很多很多当时根本理解不了的 UnicodeError,所以一开始写代码就用 Python 3.0 啦 =w=

回复

fantix 作者 · 2014年03月05日

呵呵,这个 feeds 的链接我也是有次想找找不着生拼出来的

回复

PynixWang · 2014年04月05日

还是维护两个分支吧。。。。同时兼容2和3太痛苦。。。

回复

fantix 作者 · 2014年04月06日

是呢,可是原作者希望这样~~所以我做了 https://github.com/decentfox/gevent3 呵呵~~

回复

PynixWang · 2014年04月06日

3x大法好 天灭2.x,升3保平安,人在做,天在看,字节数组留祸患,编码错误天地灭,unicode保平安。

回复

fantix 作者 · 2014年04月06日

@_@ 呵呵

回复

PynixWang · 2014年04月06日

哈哈哈哈。。。

回复

Hsiaoming_Yang · 2014年06月13日

回复

JackPy · 2015年05月07日

为毛到现在了,gevent3到现在还没问世? 能否完全抛开py2 的语法羁绊,独立实现一个py3版本的? 还有博主,你的那个 tulipcore目前啥情况,说基于asyncio来实现?好像你自己都已经放弃了。。。

回复

JackPy · 2015年05月07日

有了这个冉冉升起的pulsar(https://github.com/quantmind/pulsar),gevent 可以放到一边了

回复

载入中...