把項(xiàng)目從Python2.x移植到Python3.x的經(jīng)驗(yàn)總結(jié)
經(jīng)歷移植jinja2到python3的痛苦之后,我把項(xiàng)目暫時放一放,因?yàn)槲遗麓蚱苝ython3的兼容。我的做法是只用一個python2的代碼庫,然后在安裝的時候用2to3工具翻譯成python3。不幸的是哪怕一點(diǎn)點(diǎn)的改動都會打破迭代開發(fā)。如果你選對了python的版本,你可以專心做事,幸運(yùn)的避免了這個問題。
來自MoinMoin項(xiàng)目的Thomas Waldmann通過我的python-modernize跑jinja2,并且統(tǒng)一了代碼庫,能同時跑python2,6,2,7和3.3。只需小小清理,我們的代碼就很清晰,還能跑在所有的python版本上,并且看起來和普通的python代碼并無區(qū)別。
受到他的啟發(fā),我一遍又一遍的閱讀代碼,并開始合并其他代碼來享受統(tǒng)一的代碼庫帶給我的快感。
下面我分享一些小竅門,可以達(dá)到和我類似的體驗(yàn)。
放棄python 2.5 3.1和3.2
這是最重要的一點(diǎn),放棄2.5比較容易,因?yàn)楝F(xiàn)在基本沒人用了,放棄3.1和3.2也沒太大問題,應(yīng)為目前python3用的人實(shí)在是少得可憐。但是你為什么放棄這幾個版本呢?答案就是2.6和3.3有很多交叉哦語法和特性,代碼可以兼容這兩個版本。
- 字符串兼容。2.6和3.3支持相同的字符串語法。你可以用 "foo" 表示原生字符串(2.x表示byte,3.x表示unicode),u"foo" 表示unicode字符串,b"foo" 表示原生字符串或字節(jié)數(shù)組。
- print函數(shù)兼容,如果你的print語句比較少,那你可以加上"from __future__ import print_function",然后開始使用print函數(shù),而不是把它綁定到別的變量上,進(jìn)而避免詭異的麻煩。
- 兼容的異常語法。Python 2.6引入的 "except Exception as e" 語法也是3.x的異常捕捉語法。
- 類修飾器都有效。這個可以用在修改接口而不在類結(jié)構(gòu)定義中留下痕跡。例如可以修改迭代方法名字,也就是把 next 改成 __next__ 或者把 __str__ 改成 __unicode__ 來兼容python 2.x。
- 內(nèi)置next調(diào)用__next__或next。這點(diǎn)很有用,因?yàn)樗麄兒椭苯诱{(diào)用方法的速度差不多,所以你不用考慮得太多而去加入運(yùn)行時檢查和包裝一個函數(shù)。
- Python 2.6 加入了和python 3.3接口一樣的bytearray類型。這點(diǎn)也很有用,因?yàn)?.6沒有 3.3的byteobject類型,雖然有一個內(nèi)建的名字但那僅僅只是str的別名,并且使用習(xí)慣也有很大差異。
- Python 3.3又加入了byte到byte和string到string的編碼與解碼,這已經(jīng)在3.1和3.2中去掉了,很不幸,他們的接口很復(fù)雜了,別名也沒了,但至少更比以前的2.x版本更接近了。
最后一點(diǎn)在流編碼和解碼的時候很有用,這功能在3.0的時候去掉了,直到3.3才恢復(fù)。
沒錯,six模塊可以讓你走得遠(yuǎn)一點(diǎn),但是不要低估了代碼工整度的意義。在Python3移植過程中,我?guī)缀鯇inja2失去了興趣,因?yàn)榇a開始虐我。就算能統(tǒng)一代碼庫,但還是看起來很不舒服,影響視覺(six.b('foo')和six.u('foo')到處飛)還會因?yàn)橛?to3迭代開發(fā)帶來不必要的麻煩。不用去處理這些麻煩,回到編碼的快樂享受中吧。jinja2現(xiàn)在的代碼非常清晰,你也不用當(dāng)心python2和3的兼容問題,不過還是有一些地方使用了這樣的語句:if PY2:。
接下來假設(shè)這些就是你想支持的python版本,試圖支持python2.5,這是一個痛苦的事情,我強(qiáng)烈建議你放棄吧。支持3.2還有一點(diǎn)點(diǎn)可能,如果你能在把函數(shù)調(diào)用時把字符串都包裝起來,考慮到審美和性能,我不推薦這么做。
跳過six
six是個好東西,jinja2開始也在用,不過最后卻不給力了,因?yàn)橐浦驳絧ython3的確需要它,但還是有一些特性丟失了。你的確需要six,如果你想同時支持python2.5,但從2.6開始就沒必要使用six了,jinja2搞了一個包含助手的兼容模塊。包括很少的非python3 代碼,整個兼容模塊不足80行。
因?yàn)槠渌麕旎蛘唔?xiàng)目依賴庫的原因,用戶希望你能支持不同版本,這是six的確能為你省去很多麻煩。
開始使用Modernize
使用python-modernize移植python是個很好的還頭,他像2to3一樣運(yùn)行的時候生成代碼。當(dāng)然,他還有很多bug,默認(rèn)選項(xiàng)也不是很合理,可以避免一些煩人的事情,然你走的更遠(yuǎn)。但是你也需要檢查一下結(jié)果,去掉一些import 語句和不和諧的東西。
修復(fù)測試
做其他事之前先跑一下測試,保證測試還能通過。python3.0和3.1的標(biāo)準(zhǔn)庫就有很多問題是詭異的測試習(xí)慣改變引起的。
寫一個兼容的模塊
因此你將打算跳過six,你能夠完全拋離幫助文檔么?答案當(dāng)然是否定的。你依然需要一個小的兼容模塊,但是它足夠小,使得你能夠?qū)⑺鼉H僅放在你的包中,下面是一個基本的例子,關(guān)于一個兼容模塊看起來是個什么樣子:
import sys PY2 = sys.version_info[0] == 2 if not PY2: text_type = str string_types = (str,) unichr = chr else: text_type = unicode string_types = (str, unicode) unichr = unichr
那個模塊確切的內(nèi)容依賴于,對于你有多少實(shí)際的改變。在Jinja2中,我在這里放了一堆的函數(shù)。它包括ifilter, imap以及類似itertools的函數(shù),這些函數(shù)都內(nèi)置在3.x中。(我糾纏Python 2.x函數(shù),是為了讓讀者能夠?qū)Υa更清楚,迭代器行為是內(nèi)置的而不是缺陷) 。
為2.x版本做測試而不是3.x
總體上來說你現(xiàn)在正在使用的python是2.x版本的還是3.x版本的是需要檢查的。在這種情況下我推薦你檢查當(dāng)前版本是否是python2而把python3放到另外一個判斷的分支里。這樣等python4面世的時候你收到的“驚喜”對你的影響會小一點(diǎn)
好的處理:
if PY2: def __str__(self): return self.__unicode__().encode('utf-8')
相比之下差強(qiáng)人意的處理:
if not PY3: def __str__(self): return self.__unicode__().encode('utf-8')
字符串處理
Python 3的最大變化毫無疑問是對Unicode接口的更改。不幸的是,這些更改在某些地方非常的痛苦,而且在整個標(biāo)準(zhǔn)庫中還得到了不一致地處理。大多數(shù)與字符串處理相關(guān)的時間函數(shù)的移植將完全被廢止。字符串處理這個主題本身就可以寫成完整的文檔,不過這兒有移植Jinja2和Werkzeug所遵循的簡潔小抄:
'foo'這種形式的字符串總指的是本機(jī)字符串。這種字符串可以用在標(biāo)識符里、源代碼里、文件名里和其他底層的函數(shù)里。另外,在2.x里,只要限制這種字符串僅僅可使用ASCII字符,那么就允許作為Unicode字符串常量。
這個屬性對統(tǒng)一編碼基礎(chǔ)是非常有用的,因?yàn)镻ython 3的正常方向時把Unicode引進(jìn)到以前不支持Unicode的某些接口,不過反過來卻從不是這樣的。由于這種字符串常量“升級”為Unicode,而2.x仍然在某種程度上支持Unicode,因此這種字符串常量怎么用都行。
例如 datetime.strftime函數(shù)在Python2里嚴(yán)格不支持Unicode,并且只在3.x里支持Unicode。不過因?yàn)榇蠖鄶?shù)情況下2.x上的返回值只是ASCII編碼,所以像這樣的函數(shù)在2.x和3.x上都確實(shí)運(yùn)行良好。
>>> u'<p>Current time: %s' % datetime.datetime.utcnow().strftime('%H:%M') u'<p>Current time: 23:52'
傳遞給strftime的字符串是本機(jī)字符串(在2.x里是字節(jié),而在3.0里是Unicode)。返回值也是本機(jī)字符串并且僅僅是ASCII編碼字符。 因此在2.x和3.x上一旦對字符串進(jìn)行格式化,那么結(jié)果就一定是Unicode字符串。
u'foo'這種形式的字符串總指的是Unicode字符串,2.x的許多庫都已經(jīng)有非常好的支持Unicode,因此這樣的字符串常量對許多人來說都不應(yīng)該感到奇怪。
b'foo'這種形式的字符串總指的是只以字節(jié)形式存儲的字符串。由于2.6確實(shí)沒有類似Python 3.3所具有的字節(jié)對象,而且Python 3.3缺乏一個真正的字節(jié)字符串,因此這種常量的可用性確實(shí)受到小小的限制。當(dāng)與在2.x和3.x上具有同樣接口的字節(jié)數(shù)組對象綁定在一起時候,它立刻變得更可用了。
由于這種字符串是可以更改的,因此對原始字節(jié)的更改是非常有效的,然后你再次通過使用inbytes()封裝最終結(jié)果,從而轉(zhuǎn)換結(jié)果為更易讀的字符串。
除了這些基本的規(guī)則,我還對上面我的兼容模塊添加了 text_type,unichr 和 string_types 等變量。通過這些有了大的變化:
- isinstance(x, basestring) 變成 isinstance(x, string_types).
- isinstance(x, unicode) 變成 isinstance(x, text_type).
- isinstance(x, str) 為表明捕捉字節(jié)的意圖,現(xiàn)在變成 isinstance(x, bytes) 或者 isinstance(x, (bytes, bytearray)).
我還創(chuàng)建了一個 implements_to_string 裝飾類,來幫助實(shí)現(xiàn)帶有 __unicode__ 或 __str__ 的方法的類:
if PY2: def implements_to_string(cls): cls.__unicode__ = cls.__str__ cls.__str__ = lambda x: x.__unicode__().encode('utf-8') return cls else: implements_to_string = lambda x: x
這個想法是,你只要按2.x和3.x的方式實(shí)現(xiàn) __str__,讓它返回Unicode字符串(是的,在2.x里看起來有點(diǎn)奇怪),裝飾類在2.x里會自動把它重命名為 __unicode__,然后添加新的 __str__ 來調(diào)用 __unicode__ 并把其返回值用 UTF-8 編碼再返回。在過去,這種模式在2.x的模塊中已經(jīng)相當(dāng)普遍。例如 Jinja2 和 Django 中都這樣用。
下面是一個這種用法的實(shí)例:
@implements_to_string class User(object): def __init__(self, username): self.username = username def __str__(self): return self.username
元類語法的更改
由于Python 3更改了定義元類的語法,并且以一種不兼容的方式調(diào)用元類,所以這使移植比未更改時稍稍難了些。Six有一個with_metaclass函數(shù)可以解決這個問題,不過它在繼承樹中產(chǎn)生了一個虛擬類。對Jinjia2移植來說,這個解決方案令我非常 的不舒服,我稍稍地對它進(jìn)行了修改。這樣對外的API是相同的,只是這種方法使用臨時類與元類相連接。 好處是你使用它時不必?fù)?dān)心性能會受影響并且讓你的繼承樹保持得很完美。
這樣的代碼理解起來有一點(diǎn)難。 基本的理念是利用這種想法:元類可以自定義類的創(chuàng)建并且可由其父類選擇。這個特殊的解決方法是用元類在創(chuàng)建子類的過程中從繼承樹中刪除自己的父類。最終的結(jié)果是這個函數(shù)創(chuàng)建了帶有虛擬元類的虛擬類。一旦完成創(chuàng)建虛擬子類,就可以使用虛擬元類了,并且這個虛擬元類必須有從原始父類和真正存在的元類創(chuàng)建新類的構(gòu)造方法。這樣的話,既是虛擬類又是虛擬元類的類從不會出現(xiàn)。
這種解決方法看起來如下:
def with_metaclass(meta, *bases): class metaclass(meta): __call__ = type.__call__ __init__ = type.__init__ def __new__(cls, name, this_bases, d): if this_bases is None: return type.__new__(cls, name, (), d) return meta(name, bases, d) return metaclass('temporary_class', None, {}) 下面是你如何使用它: class BaseForm(object): pass class FormType(type): pass class Form(with_metaclass(FormType, BaseForm)): pass
字典
Python 3里更令人懊惱的更改之一就是對字典迭代協(xié)議的更改。Python2里所有的字典都具有返回列表的keys()、values()和items(),以及返回迭代器的iterkeys(),itervalues()和iteritems()。在Python3里,上面的任何一個方法都不存在了。相反,這些方法都用返回視圖對象的新方法取代了。
keys()返回鍵視圖,它的行為類似于某種只讀集合,values()返回只讀容器并且可迭代(不是一個迭代器!),而items()返回某種只讀的類集合對象。然而不像普通的集合,它還可以指向易更改的對象,這種情況下,某些方法在運(yùn)行時就會遇到失敗。
站在積極的一方面來看,由于許多人沒有理解視圖不是迭代器,所以在許多情況下,你只要忽略這些就可以了。
Werkzeug和Dijango實(shí)現(xiàn)了大量自定義的字典對象,并且在這兩種情況下,做出的決定僅僅是忽略視圖對象的存在,然后讓keys()及其友元返回迭代器。
由于Python解釋器的限制,這就是目前可做的唯一合理的事情了。不過存在幾個問題:
- 視圖本身不是迭代器這個事實(shí)意味著通常狀況下你沒有充足的理由創(chuàng)建臨時對象。
- 內(nèi)置字典視圖的類集合行為在純Python里由于解釋器的限制不可能得到復(fù)制。
- 3.x視圖的實(shí)現(xiàn)和2.x迭代器的實(shí)現(xiàn)意味著有大量重復(fù)的代碼。
下面是Jinja2編碼庫常具有的對字典進(jìn)行迭代的情形:
if PY2: iterkeys = lambda d: d.iterkeys() itervalues = lambda d: d.itervalues() iteritems = lambda d: d.iteritems() else: iterkeys = lambda d: iter(d.keys()) itervalues = lambda d: iter(d.values()) iteritems = lambda d: iter(d.items())
為了實(shí)現(xiàn)類似對象的字典,類修飾符再次成為可行的方法:
if PY2: def implements_dict_iteration(cls): cls.iterkeys = cls.keys cls.itervalues = cls.values cls.iteritems = cls.items cls.keys = lambda x: list(x.iterkeys()) cls.values = lambda x: list(x.itervalues()) cls.items = lambda x: list(x.iteritems()) return cls else: implements_dict_iteration = lambda x: x
在這種情況下,你需要做的一切就是把keys()和友元方法實(shí)現(xiàn)為迭代器,然后剩余的會自動進(jìn)行:
@implements_dict_iteration class MyDict(object): ... def keys(self): for key, value in iteritems(self): yield key def values(self): for key, value in iteritems(self): yield value def items(self): ...
通用迭代器的更改
由于一般性地更改了迭代器,所以需要一丁點(diǎn)的幫助就可以使這種更改毫無痛苦可言。真正唯一的更改是從next()到__next__的轉(zhuǎn)換。幸運(yùn)的是這個更改已經(jīng)經(jīng)過透明化處理。 你唯一真正需要更改的事情是從x.next()到next(x)的更改,而且剩余的事情由語言來完成。
如果你計(jì)劃定義迭代器,那么類修飾符再次成為可行的方法了:
if PY2: def implements_iterator(cls): cls.next = cls.__next__ del cls.__next__ return cls else: implements_iterator = lambda x: x 為了實(shí)現(xiàn)這樣的類,只要在所有的版本里定義迭代步長方法__next__就可以了: @implements_iterator class UppercasingIterator(object): def __init__(self, iterable): self._iter = iter(iterable) def __iter__(self): return self def __next__(self): return next(self._iter).upper()
轉(zhuǎn)換編解碼器
Python 2編碼協(xié)議的優(yōu)良特性之一就是它不依賴于類型。 如果你愿意把csv文件轉(zhuǎn)換為numpy數(shù)組的話,那么你可以注冊一個這樣的編碼器。然而自從編碼器的主要公共接口與字符串對象緊密關(guān)聯(lián)后,這個特性不再為眾人所知。由于在3.x里轉(zhuǎn)換的編解碼器變得更為嚴(yán)格,所以許多這樣的功能都被刪除了,不過后來由于證明轉(zhuǎn)換編解碼有用,在3.3里重新引入了?;旧蟻碚f,所有Unicode到字節(jié)的轉(zhuǎn)換或者相反的轉(zhuǎn)換的編解碼器在3.3之前都不可用。hex和base64編碼就位列與這些編解碼的之中。
下面是使用這些編碼器的兩個例子:一個是字符串上的操作,一個是基于流的操作。前者就是2.x里眾所周知的str.encode(),不過,如果你想同時支持2.x和3.x,那么由于更改了字符串API,現(xiàn)在看起來就有些不同了:
>>> import codecs >>> codecs.encode(b'Hey!', 'base64_codec') 'SGV5IQ==\n'
同樣,你將注意到在3.3里,編碼器不理解別名,要求你書寫編碼別名為"base64_codec"而不是"base64"。
(我們優(yōu)先選擇這些編解碼器而不是選擇binascii模塊里的函數(shù),因?yàn)橥ㄟ^對這些編碼器增加編碼和解碼,就可以支持所增加的編碼基于流的操作。)
其他注意事項(xiàng)
仍然有幾個地方我尚未有良好的解決方案,或者說處理這些地方常常令人懊惱,不過這樣的地方會越來越少。不幸是的這些地方的某些現(xiàn)在已經(jīng)是Python 3 API的一部分,并且很難被發(fā)現(xiàn),直到你觸發(fā)一個邊緣情形的時候才能發(fā)現(xiàn)它。
在Linux上處理文件系統(tǒng)和文件IO訪問仍然令人懊惱,因?yàn)樗皇腔赨nicode的。Open()函數(shù)和文件系統(tǒng)的層都有危險的平臺指定的缺省選項(xiàng)。例如,如果我從一臺de_AT的機(jī)器SSH到一臺en_US機(jī)器,那么Python對文件系統(tǒng)和文件操作就喜歡回退到ASCII編碼上。
我注意到通常Python3上對文本操作最可靠的同時也在2.x正常工作的方法是僅僅以二進(jìn)制模式打開文件,然后顯式地進(jìn)行解碼。另外,你也可以使用2.x上的codec.open或者io.open函數(shù),以及Python 3上內(nèi)置的帶有編碼參數(shù)的Open函數(shù)。
標(biāo)準(zhǔn)庫里的URL不能用Unicode正確地表示,這使得一些URL在3.x里不能被正確的處理。
由于更改了語法,所以追溯對象產(chǎn)生的異常需要輔助函數(shù)。通常來說這非常罕見,而且很容易處理。下面是由于更改了語法所遇到的情形之一,在這種情況下,你將不得不把代碼移到exec塊里。
if PY2: exec('def reraise(tp, value, tb):\n raise tp, value, tb') else: def reraise(tp, value, tb): raise value.with_traceback(tb)
如果你有部分代碼依賴于不同的語法的話,那么通常來說前面的exec技巧是非常有用的。不過現(xiàn)在由于exec本身就有不同的語法,所以你不能用它來執(zhí)行任何命名空間上的操作。下面給出的代碼段不會有大問題,因?yàn)榘裞ompile用做嵌入函數(shù)的eval可運(yùn)行在兩個版本上。另外你可以通過exec本身啟動一個exec函數(shù)。
exec_ = lambda s, *a: eval(compile(s, '<string>', 'exec'), *a)
如果你在Python C API上書寫了C模塊,那么自殺吧。從我知道那刻起到仙子仍然沒有工具可處理這個問題,而且許多東西都已經(jīng)更改了。借此機(jī)會放棄你構(gòu)造模塊所使用的這種方法,然后在cffi或者ctypes上重新書寫模塊。如果這種方法還不行的話,因?yàn)槟阌悬c(diǎn)頑固,那么只有接受這樣的痛苦。也許試著在C預(yù)處理器上書寫某些令人討厭的事可以使移植更容易些。
使用Tox來進(jìn)行本地測試。能夠立刻在所有Python版本上運(yùn)行你的測試是非常有益的,這將為你找到許多問題。
展望
統(tǒng)一2.x和3.x的基本編碼庫現(xiàn)在確實(shí)可以開始了。移植的大量時間仍然將花費(fèi)在試圖解決有關(guān)Unicode以及與其他可能已經(jīng)更改了自身API的模塊交互時API是如何操作上。無論如何,如果你打算考慮移植庫的話,那么請不要觸碰2.5以下的版本、3.0-3.2版本,這樣的話將不會對版本造成太大的傷害。
相關(guān)文章
python3翻轉(zhuǎn)字符串里的單詞點(diǎn)的實(shí)現(xiàn)方法
這篇文章主要介紹了python3翻轉(zhuǎn)字符串里的單詞點(diǎn)的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Python 實(shí)現(xiàn)一個簡單的web服務(wù)器
這篇文章主要介紹了Python 實(shí)現(xiàn)一個簡單的web服務(wù)器的方法,幫助大家更好的理解和學(xué)習(xí)python,感興趣的朋友可以了解下2021-01-01關(guān)于Python中flask-httpauth庫用法詳解
這篇文章主要介紹了關(guān)于Python中flask-httpauth庫用法詳解,Flask-HTTPAuth是一個?Flask?擴(kuò)展,它簡化了?HTTP?身份驗(yàn)證與?Flask?路由的使用,需要的朋友可以參考下2023-04-04Django認(rèn)證系統(tǒng)user對象實(shí)現(xiàn)過程解析
這篇文章主要介紹了Django認(rèn)證系統(tǒng)user對象實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03