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

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
