gevent 遷移 Python 3 歷程(一)

時隔一年多,gevent 的做者 Denis Bilenko 終於從創業的百忙之中,抽出時間打算 review 我在 2012 年的時候完成的 gevent 到 Python 3 的遷移工做html

Skype 交談中,Denis 問了幾個問題,我發現有很多改動我已經忘記了當初寫的緣由了,這個案例教育咱們,在作較大的修改的時候,儘可能拆分紅多個較小的提交,每一個提交消息都儘可能寫清楚。^_^python

由於過了一年多,gevent 的 master 上也改動了一些。我嘗試了作 merge,發現結果不是很理想,再加上對當時修改又不是很滿意了,因而乎,我選擇了參考原來的改動,從新遷移一次。git

插敘一段小插曲。其實在 Denis 聯繫我以前,我已經放棄他了——由於他實在是好久好久沒有在 gevent 上活躍開發了,gevent 1.0 感受也是憋了很久憋出來的。當時連蟒爹的 Tulip/asyncio 都眼瞅着要發佈了,我就直接 fork 了個項目叫 gevent3,也就是 Python 3 版的、基於 asyncio 的 gevent,這個 gevent3 有機會再跟你們介紹。沒想到剛 fork 完沒作多久,就發生了故事開頭寫的事情。github

言歸正傳。接下來我分段介紹我這幾個月用業餘時間幾乎作完的第二次遷移工做,但願能對也在作向 Python 3 遷移工做的同窗們有點幫助。python2.7

Denis 對遷移工做的要求是,用同一套代碼,同時支持 Python 2.6, 2.7 和 3.3。除了 greenlet,最好不要再引入其餘的依賴,甚至是 six——一個專一於解決用同一套代碼支持不一樣 Python 版本問題的庫。socket

軟柿子

老虎吃天,無從下口。面對龐大的代碼量,還得先撿軟柿子捏。async

好比說,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)

相似的軟柿子還有:google

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

相關文章
相關標籤/搜索