編寫相容Python2.x與3.x程式碼
當我們正處於Python 2.x到Python 3.x的過渡期時,你可能想過是否可以在不修改任何程式碼的前提下能同時執行在Python 2和3中。這看起來還真是一個合理的訴求,但如何開始呢?哪些Python 2 程式碼在 3.x 直譯器執行時容易出狀況呢?
print vs print()
如果你想的和我一樣,你或許會說print語句,這是個很好的著手點,先簡單展示一下,print在2.x中是一條語句,而在3.x中它是一個關鍵字或者是保留字。換句話說,因為這個變化涉及到語言的語法,你不可以使用在if語句中,Python仍然沒有#ifdef 巨集。下面嘗試把括號裡面的引數列印出來:
1 2 |
>>> print('Hello World!') Hello World! |
很酷,這個在Python2和Python3中都可以執行,而且執行的效果是一樣的,再來看看下面這段:
1 2 |
>>> print(10, 20) # Python 2 (10, 20) |
此時,你並沒有像前面那樣幸運得到一樣的結果,Python2中列印的是元組(tuple),而在Python3中傳遞多個引數到print()裡面時列印的是兩個值:
1 2 |
>>> print(10, 20) # Python 3 10 20 |
如果你思考得比較多的話,我們可以檢查print是否是一個關鍵字,keyword模組包含一個關鍵字列表。print在3.x中不是關鍵字,可以簡單驗證一下:
1 2 3 |
>>> import keyword >>> 'print' in keyword.kwlist False |
作為一名聰明的程式設計師,你可能在2.x中嘗試的時候期待的結果是True,儘管這並沒有錯,但是為了達到Python3的效果,但你仍然會因為其他原因導致失敗。
1 2 3 4 5 6 |
>>> import keyword >>> if 'print' in keyword.kwlist: ... from __future__ import print_function ... File "", line 2 SyntaxError: from __future__ imports must occur at the beginning of the file |
一種解決方案是使用一個函式,其功能類似於print,其中之一是sys.stdout.write(),另一個是distutils.log.warn()。不管出於什麼原因,我們決定使用後者。“hello world”的例子看起來是這樣的:
1 2 3 4 |
# Python 2.x print 'Hello World!' # Python 3.x print('Hello World!') |
下面的程式碼就可以在兩個版本中通用:
1 2 3 |
# Python 2.x & 3.x compatible from distutils.log import warn as printf printf('Hello World!') |
為什麼我們不用sys.stdout.write()呢,因為我們需要新增一個NEWLINE字元在字串的結尾來相容這種行為(python2.x中write方法不會換行):
1 2 3 |
# Python 2.x & 3.x compatible import sys sys.stdout.write('Hello World!n') |
Import your way to a solution
一般情況情況下,import時沒什麼煩惱,只要正確的匯入就行,但在下面程式碼中,我們想匯入urlopen()函式,在Python2中,他同時存在與urllib2和urllib2中(我們使用後者),在Python3中,他被整合到了urllib.request中,而你的方案是要既能在2.x和3.x中正常工作:
1 2 3 4 |
try: from urllib2 import urlopen except ImportError: from urllib.request import urlopen |
出於對記憶體的保護,也許你對iterator(Python3)版本的zip()更加有興趣,在Python2中,iterator版本是itertools.izip()。這個函式在Python3中被重新命名替換成了zip()。如果你使用迭代版本,匯入語句也非常直白:
1 2 3 4 |
try: from itertools import izip as zip except ImportError: pass |
另一個列子是看來來並不怎麼優雅的StringIO類,在Python2中,純Python版本是StringIO模組,意味著訪問的時候是通過StringIO.StringIO,同樣還有一個更為快速的C語言版本,位於cStringIO.StringIO,不過這取決你的Python安裝版本,你可以優先使用cStringIO然後是StringIO(如果cStringIO不能用的話)。在Python3中,Unicode是預設的string型別,但是如果你做任何和網路相關的操作,很有可能你不得不用ASCII/位元組字串來操作,所以代替StringIO,你要io.BytesIO,為了達到你想要的,這個匯入看起來有點醜:
1 2 3 4 5 6 7 |
try: from io import BytesIO as StringIO except ImportError: try: from cStringIO import StringIO except ImportError: from StringIO import StringIO |
Putting it all together
如果你運氣好的話,上面那些就是你要準備做的全部,剩下的程式碼都比開始設定的地方更簡單。如果你按照上面的方式匯入了distutils.log.warn()[printf()],url*urlopen()
,*.StringIO
和一個標準的匯入:xml.etree.ElementTree
(2.5及更新的),現在你就可以寫一個非常簡短短的解析器來展示從Google News服務中提供的頭條故事(譯註:當然首先得備一個梯子),只需八行程式碼:
1 2 3 4 5 6 7 8 9 |
g = urlopen('http://news.google.com/news?topic=h&output=rss') f = StringIO(g.read()) g.close() tree = xml.etree.ElementTree.parse(f) f.close() for elmt in tree.getiterator(): if elmt.tag == 'title' and not elmt.text.startswith('Top Stories'): printf('- %s' % elmt.text) |
這段指令碼在2.x和3.x下面執行時,不需要做任何改動,執行效果完全一樣,當然,如果你正在使用的是2.4或者更老的版本,你需要單獨下載ElementTree。
但是有時候感覺這些改變把你優雅的Python程式碼弄得一團糟,畢竟可讀性才是最重要的,如果你要優先保證程式碼的整潔而且在不修改任何地方的前提下執行在兩個版本的Python環境中,那麼你可以看一下six包。
six一個相容庫,它的主要任務是提供介面隱藏複雜的細節,你可以在這裡找到它。無論你是使用像six這樣的庫還是用自己的方法來做,我們希望這個簡短的介紹可以讓你開始考慮寫的程式碼能夠在2.x和3.x下同時執行。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!