若是你仍在使用 2.x,那麼是時候將你的代碼移植到 Python 3 了。html
在技術的長河中,軟件、工具、系統等版本的迭代本是常事,但因爲使用習慣、版本的兼容性、易用性等因素,不少用戶及開發者在使用或作開發的過程當中,並不肯意及時同步更新,而這無疑爲 IT 技術圈帶來了諸多碎片化問題,正如系統之 Android、Windows,也正如編程語言之 Python。python
近日,Python 由於其版本的碎片化問題遭到了英國國家網絡安全中心(NCSC)的點名,NCSC 警告開發者們必須考慮將 Python 2.x 的代碼庫移植到 Python 3.x 版本上,主要是由於自 2020 年 1 月 1 日起,Python 2.x 將走到其生命的盡頭,再也不獲得支持(EOL,End-of-life)。與此同時,NCSC 還將繼續使用 Python 2.x 的公司比做 EOL 以引誘另外一個 WannaCry(病毒)或 Equifax(信息泄露)事件的發生。git
Python 的應用現狀程序員
回望近些年才被 AI 點燃的 Python,其實並不是是一門新的語言,它最先於 1989 年末由知名的荷蘭計算機程序員 Guido van Rossum 發明,後來做爲一門面向對象、直譯式計算機程序設計語言於 1991 年面世。其 30 年的發展歷程可謂比編程語言界的常青藤 Java 更爲久遠。github
而論及 Java,一年兩次迭代的速度早已讓很多開發者痛苦不堪,其紛紛開啓對 Oracle 的吐槽模式,並直言「立刻推出 JDK 13 的你只管更新,不用顧及咱們的感覺,總之咱們還堅守在 JDK 1.x」。事實上,不止 Java,Python 也有着相同的問題,不少人對 Python 舊版本的堅持反而也讓該語言的核心開發者們也備受煎熬,由於舊版本在安全性、功能上均沒法與新版本相媲美,容易出現不少 Bug。編程
根據著名 IDE 開發商 JetBrains 和 Python 基金會於今年年初發布的《Python Developers Survey 2018 Results》報告顯示,Python 3 的採用率正在快速增加,將其做爲主要解釋器的開發者比例從 2017 年的 75% 上升到了 84%,不過與此同時,Python 2 仍佔有 16% 的份額。數組
其中,在 Python 2.x 版本中,Python 2.7 最受歡迎且使用的人數最多,佔比 93%。安全
那麼這些開發者究竟爲什麼不肯意升級?網絡
Python 的版本之過編程語言
一直以來,語法簡單、擁有豐富和強大類庫的 Python 被稱之爲一門膠水語言,它可以很輕鬆的把用其它語言製做的各類模塊(尤爲是 C/C++)輕鬆地聯結在一塊兒。
不過在版本的迭代過程當中,Python 出現了一個常常被開發者們詬病的問題,即於 2008 年發佈的 Python 3 在設計時沒有考慮向較早版本相容的問題,Python 2.x 版本與 Python 3.x 之間並不兼容。這意味着若是你的軟件是基於 Python 2 開發的,想要遷移到 Python 3 上面,無疑須要耗費巨大的成本。並且在此過程當中,若是項目涉及到諸多關於 Python 2 的類庫,可能還會致使遷移失敗。
而自此問題的出現讓很多本來想要升級的開發者寧願停留在之前的舊版本中,對此,有很多網友表示:
Python 2.x 和 Python 3.x 二者在編碼效率上沒有明顯差距,可是 Python 3.x 卻要花額外的成本處理兼容性問題;感受 Python 2 和 Python 3 是兩門不一樣的語言,只不過他們的語法類似罷了;......除此以外,根據來自 Python 社區開發和共享軟件的存儲中心 Python Package Index 統計顯示,當前主流的 Python 軟件包中仍然有很多使用的是 Python 2.x 版本。且其中,每一個包每月的下載量高達百萬次。而想要將這些包移植到 Python 3 上,也絕非是一件易事。
Python 2.x 淘汰乃大勢所趨
誠然開發者有多少個不肯意,但 Python 2.x 淘汰已成必然趨勢。早在 2018 年 3 月,Python 之父 Guido van Rossum 就曾在郵件列表上宣佈 Python 2.7 將於 2020 年 1 月 1 日終止支持,這意味着以後 Python 2 將再也不被統一維護,與之對應的是主流第三方庫也不會再提供針對 Python 2 版本的開發支持。不過,想要繼續使用舊版本也並不是不可,就如同 Java 同樣,交付商業費用便可,但這樣的作法在突飛猛進的技術圈中,顯然不是長久之計。
現在 NCSC 的警醒,再次告誡開發者們,「若是繼續使用不受支持的模塊,公司就會冒着組織和數據的安全性風險,由於漏洞早晚會出現,並且沒人會修復。」
與此同時,來自 NCSC 的平臺安全研究員 Rich M 也於官方博客上列舉了不升級 Python 2 將面臨的種種問題:
依賴項
許多流行的項目,如 NumPy、Requests 和 TensorFlow 等承諾到 2020 年將中止支持 2.x,而且當前一些項目已經這麼作了。
這意味着若是你想使用你喜歡模塊的最新功能,那麼就須要使用 Python 3。等待更新的時間越長,到時將更改的依賴項的 Python 3 版本會越多,更新起來會變得越困難。
或將阻礙其餘開發者
若是你正在維護其餘開發者所依賴的庫,則可能會阻止他們更新到 Python 3。若是阻礙其餘開發者,你會在間接、可能無心中加大其餘項目面臨的安全風險。
你也許不在公司外部發布任何代碼,但要考慮可能也在內部使用你代碼的同事。
錯失最新的 Python 功能
表達式的收益——容許生成器將其部分操做委託給另外一個生成器。Unicode 字符串——Unicode 處理起來更容易。打印函數——打印函數有額外的功能,使其更靈活。視圖和迭代器取代列表——一些衆所周知的 API 再也不返回列表。好比說,字典返回鍵、值或二者的視圖。「multi-with」語句——複雜的 with 語句更易於閱讀。使用 * 和 ** 解包——擴展 * 可迭代解包運算符和 ** 字典解包運算符的用途。如今能夠在函數調用中使用任意數量的解包運算符。純關鍵字實參——容許實參出如今 varargs 參數的後面。F 字符串——運行時評估的一種新類型的字符串常量,可能含有任何有效的Python表達式。大量的加速和優化機制。
Python 2.x 如何遷移到 Python 3.x?
經歷移植jinja2到python3的痛苦以後,我把項目暫時放一放,由於我怕打破python3的兼容。個人作法是隻用一個python2的代碼庫,而後在安裝的時候用2to3工具翻譯成python3。不幸的是哪怕一點點的改動都會打破迭代開發。若是你選對了python的版本,你能夠專心作事,幸運的避免了這個問題。
來自MoinMoin項目的Thomas Waldmann經過個人python-modernize跑jinja2,而且統一了代碼庫,能同時跑python2,6,2,7和3.3。只需小小清理,咱們的代碼就很清晰,還能跑在全部的python版本上,而且看起來和普通的python代碼並沒有區別。
受到他的啓發,我一遍又一遍的閱讀代碼,並開始合併其餘代碼來享受統一的代碼庫帶給個人快感。
下面我分享一些小竅門,能夠達到和我相似的體驗。
放棄python 2.x和3.2
這是最重要的一點,放棄2.5比較容易,由於如今基本沒人用了,放棄3.1和3.2也沒太大問題,應爲目前python3用的人實在是少得可憐。可是你爲何放棄這幾個版本呢?答案就是2.6和3.3有不少交叉哦語法和特性,代碼能夠兼容這兩個版本。
最後一點在流編碼和解碼的時候頗有用,這功能在3.0的時候去掉了,直到3.3才恢復。
沒錯,six模塊可讓你走得遠一點,可是不要低估了代碼工整度的意義。在Python3移植過程當中,我幾乎對jinja2失去了興趣,由於代碼開始虐我。就算能統一代碼庫,但仍是看起來很不舒服,影響視覺(six.b('foo')和six.u('foo')處處飛)還會由於用2to3迭代開發帶來沒必要要的麻煩。不用去處理這些麻煩,回到編碼的快樂享受中吧。jinja2如今的代碼很是清晰,你也不用小心python2和3的兼容問題,不過仍是有一些地方使用了這樣的語句:if PY2:。
接下來假設這些就是你想支持的python版本,試圖支持python2.5,這是一個痛苦的事情,我強烈建議你放棄吧。支持3.2還有一點點可能,若是你能在把函數調用時把字符串都包裝起來,考慮到審美和性能,我不推薦這麼作。
跳過six
six是個好東西,jinja2開始也在用,不過最後卻不給力了,由於移植到python3的確須要它,但仍是有一些特性丟失了。你的確須要six,若是你想同時支持python2.5,但從2.6開始就不必使用six了,jinja2搞了一個包含助手的兼容模塊。包括不多的非python3 代碼,整個兼容模塊不足80行。
由於其餘庫或者項目依賴庫的緣由,用戶但願你能支持不一樣版本,這是six的確能爲你省去不少麻煩。
開始使用Modernize
使用python-modernize移植python是個很好的還頭,他像2to3同樣運行的時候生成代碼。固然,他還有不少bug,默認選項也不是很合理,能夠避免一些煩人的事情,然你走的更遠。可是你也須要檢查一下結果,去掉一些import 語句和不和諧的東西。
修復測試
作其餘事以前先跑一下測試,保證測試還能經過。python3.0和3.1的標準庫就有不少問題是詭異的測試習慣改變引發的。
寫一個兼容的模塊
所以你將打算跳過six,你可以徹底拋離幫助文檔麼?答案固然是否認的。你依然須要一個小的兼容模塊,可是它足夠小,使得你可以將它僅僅放在你的包中,下面是一個基本的例子,關於一個兼容模塊看起來是個什麼樣子:
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
那個模塊確切的內容依賴於,對於你有多少實際的改變。在Jinja2中,我在這裏放了一堆的函數。它包括ifilter, imap以及相似itertools的函數,這些函數都內置在3.x中。(我糾纏Python 2.x函數,是爲了讓讀者可以對代碼更清楚,迭代器行爲是內置的而不是缺陷) 。
爲2.x版本作測試而不是3.x
整體上來講你如今正在使用的python是2.x版本的仍是3.x版本的是須要檢查的。在這種狀況下我推薦你檢查當前版本是不是python2而把python3放到另一個判斷的分支裏。這樣等python4面世的時候你收到的「驚喜」對你的影響會小一點。
好的處理方式:
1 if PY2: 2 def __str__(self): 3 return self.__unicode__().encode('utf-8')
相比之下差強人意的處理:
1 if not PY3: 2 def __str__(self): 3 return self.__unicode__().encode('utf-8')
字符串處理
Python 3的最大變化毫無疑問是對Unicode接口的更改。不幸的是,這些更改在某些地方很是的痛苦,並且在整個標準庫中還獲得了不一致地處理。大多數與字符串處理相關的時間函數的移植將徹底被廢止。字符串處理這個主題自己就能夠寫成完整的文檔,不過這兒有移植Jinja2和Werkzeug所遵循的簡潔小抄:
'foo'這種形式的字符串總指的是本機字符串。這種字符串能夠用在標識符裏、源代碼裏、文件名裏和其餘底層的函數裏。另外,在2.x裏,只要限制這種字符串僅僅可以使用ASCII字符,那麼就容許做爲Unicode字符串常量。
這個屬性對統一編碼基礎是很是有用的,由於Python 3的正常方向時把Unicode引進到之前不支持Unicode的某些接口,不過反過來卻從不是這樣的。因爲這種字符串常量「升級」爲Unicode,而2.x仍然在某種程度上支持Unicode,所以這種字符串常量怎麼用都行。
例如 datetime.strftime函數在Python2裏嚴格不支持Unicode,而且只在3.x裏支持Unicode。不過由於大多數狀況下2.x上的返回值只是ASCII編碼,因此像這樣的函數在2.x和3.x上都確實運行良好。
>>> u'<p>Current time: %s' % datetime.datetime.utcnow().strftime('%H:%M') u'<p>Current time: 23:52
傳遞給strftime的字符串是本機字符串(在2.x裏是字節,而在3.0裏是Unicode)。返回值也是本機字符串而且僅僅是ASCII編碼字符。 所以在2.x和3.x上一旦對字符串進行格式化,那麼結果就必定是Unicode字符串。
u'foo'這種形式的字符串總指的是Unicode字符串,2.x的許多庫都已經有很是好的支持Unicode,所以這樣的字符串常量對許多人來講都不該該感到奇怪。
b'foo'這種形式的字符串總指的是隻以字節形式存儲的字符串。因爲2.6確實沒有相似Python 3.3所具備的字節對象,並且Python 3.3缺少一個真正的字節字符串,所以這種常量的可用性確實受到小小的限制。當與在2.x和3.x上具備一樣接口的字節數組對象綁定在一塊兒時候,它馬上變得更可用了。
因爲這種字符串是能夠更改的,所以對原始字節的更改是很是有效的,而後你再次經過使用inbytes()封裝最終結果,從而轉換結果爲更易讀的字符串。
除了這些基本的規則,我還對上面個人兼容模塊添加了 text_type,unichr 和 string_types 等變量。經過這些有了大的變化:
我還建立了一個 implements_to_string 裝飾類,來幫助實現帶有 __unicode__ 或 __str__ 的方法的類:
1 if PY2: 2 def implements_to_string(cls): 3 cls.__unicode__ = cls.__str__ 4 cls.__str__ = lambda x: x.__unicode__().encode('utf-8') 5 return cls 6 else: 7 implements_to_string = lambda x: x
這個想法是,你只要按2.x和3.x的方式實現 __str__,讓它返回Unicode字符串(是的,在2.x裏看起來有點奇怪),裝飾類在2.x裏會自動把它重命名爲 __unicode__,而後添加新的 __str__ 來調用 __unicode__ 並把其返回值用 UTF-8 編碼再返回。在過去,這種模式在2.x的模塊中已經至關廣泛。例如 Jinja2 和 Django 中都這樣用。
下面是一個這種用法的實例:
@implements_to_string class User(object): def __init__(self, username): self.username = username def __str__(self): return self.username
元類語法的更改
因爲Python 3更改了定義元類的語法,而且以一種不兼容的方式調用元類,因此這使移植比未更改時稍稍難了些。Six有一個with_metaclass函數能夠解決這個問題,不過它在繼承樹中產生了一個虛擬類。對Jinjia2移植來講,這個解決方案令我很是 的不舒服,我稍稍地對它進行了修改。這樣對外的API是相同的,只是這種方法使用臨時類與元類相鏈接。 好處是你使用它時沒必要擔憂性能會受影響而且讓你的繼承樹保持得很完美。
這樣的代碼理解起來有一點難。 基本的理念是利用這種想法:元類能夠自定義類的建立而且可由其父類選擇。這個特殊的解決方法是用元類在建立子類的過程當中從繼承樹中刪除本身的父類。最終的結果是這個函數建立了帶有虛擬元類的虛擬類。一旦完成建立虛擬子類,就可使用虛擬元類了,而且這個虛擬元類必須有從原始父類和真正存在的元類建立新類的構造方法。這樣的話,既是虛擬類又是虛擬元類的類從不會出現。
這種解決方法看起來以下:
1 def with_metaclass(meta, *bases): 2 class metaclass(meta): 3 __call__ = type.__call__ 4 __init__ = type.__init__ 5 def __new__(cls, name, this_bases, d): 6 if this_bases is None: 7 return type.__new__(cls, name, (), d) 8 return meta(name, bases, d) 9 return metaclass('temporary_class', None, {}) 10 下面是你如何使用它: 11 12 class BaseForm(object): 13 pass 14 15 class FormType(type): 16 pass 17 18 class Form(with_metaclass(FormType, BaseForm)): 19 pass
字典
Python 3裏更使人懊惱的更改之一就是對字典迭代協議的更改。Python2裏全部的字典都具備返回列表的keys()、values()和items(),以及返回迭代器的iterkeys(),itervalues()和iteritems()。在Python3裏,上面的任何一個方法都不存在了。相反,這些方法都用返回視圖對象的新方法取代了。
keys()返回鍵視圖,它的行爲相似於某種只讀集合,values()返回只讀容器而且可迭代(不是一個迭代器!),而items()返回某種只讀的類集合對象。然而不像普通的集合,它還能夠指向易更改的對象,這種狀況下,某些方法在運行時就會遇到失敗。
站在積極的一方面來看,因爲許多人沒有理解視圖不是迭代器,因此在許多狀況下,你只要忽略這些就能夠了。
Werkzeug和Dijango實現了大量自定義的字典對象,而且在這兩種狀況下,作出的決定僅僅是忽略視圖對象的存在,而後讓keys()及其友元返回迭代器。
因爲Python解釋器的限制,這就是目前可作的惟一合理的事情了。不過存在幾個問題:
下面是Jinja2編碼庫常具備的對字典進行迭代的情形:
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())
爲了實現相似對象的字典,類修飾符再次成爲可行的方法:
1 if PY2: 2 def implements_dict_iteration(cls): 3 cls.iterkeys = cls.keys 4 cls.itervalues = cls.values 5 cls.iteritems = cls.items 6 cls.keys = lambda x: list(x.iterkeys()) 7 cls.values = lambda x: list(x.itervalues()) 8 cls.items = lambda x: list(x.iteritems()) 9 return cls 10 else: 11 implements_dict_iteration = lambda x: x
在這種狀況下,你須要作的一切就是把keys()和友元方法實現爲迭代器,而後剩餘的會自動進行:
@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): ...
通用迭代器的更改
因爲通常性地更改了迭代器,因此須要一丁點的幫助就可使這種更改毫無痛苦可言。真正惟一的更改是從next()到__next__的轉換。幸運的是這個更改已經通過透明化處理。 你惟一真正須要更改的事情是從x.next()到next(x)的更改,並且剩餘的事情由語言來完成。
若是你計劃定義迭代器,那麼類修飾符再次成爲可行的方法了:
1 if PY2: 2 def implements_iterator(cls): 3 cls.next = cls.__next__ 4 del cls.__next__ 5 return cls 6 else: 7 implements_iterator = lambda x: x 8 爲了實現這樣的類,只要在全部的版本里定義迭代步長方法__next__就能夠了: 9 10 @implements_iterator 11 class UppercasingIterator(object): 12 def __init__(self, iterable): 13 self._iter = iter(iterable) 14 def __iter__(self): 15 return self 16 def __next__(self): 17 return next(self._iter).upper()
轉換編解碼器:
Python 2編碼協議的優良特性之一就是它不依賴於類型。 若是你願意把csv文件轉換爲numpy數組的話,那麼你能夠註冊一個這樣的編碼器。然而自從編碼器的主要公共接口與字符串對象緊密關聯後,這個特性再也不爲衆人所知。因爲在3.x裏轉換的編解碼器變得更爲嚴格,因此許多這樣的功能都被刪除了,不事後來因爲證實轉換編解碼有用,在3.3裏從新引入了。基本上來講,全部Unicode到字節的轉換或者相反的轉換的編解碼器在3.3以前都不可用。hex和base64編碼就位列與這些編解碼的之中。
下面是使用這些編碼器的兩個例子:一個是字符串上的操做,一個是基於流的操做。前者就是2.x裏衆所周知的str.encode(),不過,若是你想同時支持2.x和3.x,那麼因爲更改了字符串API,如今看起來就有些不一樣了:
>>> import codecs >>> codecs.encode(b'Hey!', 'base64_codec') 'SGV5IQ==\n'
一樣,你將注意到在3.3裏,編碼器不理解別名,要求你書寫編碼別名爲"base64_codec"而不是"base64"。(咱們優先選擇這些編解碼器而不是選擇binascii模塊裏的函數,由於經過對這些編碼器增長編碼和解碼,就能夠支持所增長的編碼基於流的操做。)
其餘注意事項
仍然有幾個地方我還沒有有良好的解決方案,或者說處理這些地方經常使人懊惱,不過這樣的地方會愈來愈少。不幸是的這些地方的某些如今已是Python 3 API的一部分,而且很難被發現,直到你觸發一個邊緣情形的時候才能發現它。
在Linux上處理文件系統和文件IO訪問仍然使人懊惱,由於它不是基於Unicode的。Open()函數和文件系統的層都有危險的平臺指定的缺省選項。例如,若是我從一臺de_AT的機器SSH到一臺en_US機器,那麼Python對文件系統和文件操做就喜歡回退到ASCII編碼上。
我注意到一般Python3上對文本操做最可靠的同時也在2.x正常工做的方法是僅僅以二進制模式打開文件,而後顯式地進行解碼。另外,你也可使用2.x上的codec.open或者io.open函數,以及Python 3上內置的帶有編碼參數的Open函數。
標準庫裏的URL不能用Unicode正確地表示,這使得一些URL在3.x裏不能被正確的處理。
因爲更改了語法,因此追溯對象產生的異常須要輔助函數。一般來講這很是罕見,並且很容易處理。下面是因爲更改了語法所遇到的情形之一,在這種狀況下,你將不得不把代碼移到exec塊裏。
1 if PY2: 2 exec('def reraise(tp, value, tb):\n raise tp, value, tb') 3 else: 4 def reraise(tp, value, tb): 5 raise value.with_traceback(tb)
若是你有部分代碼依賴於不一樣的語法的話,那麼一般來講前面的exec技巧是很是有用的。不過如今因爲exec自己就有不一樣的語法,因此你不能用它來執行任何命名空間上的操做。下面給出的代碼段不會有大問題,由於把compile用作嵌入函數的eval可運行在兩個版本上。另外你能夠經過exec自己啓動一個exec函數。
exec_ = lambda s, *a: eval(compile(s, '<string>', 'exec'), *a)
若是你在Python C API上書寫了C模塊,那麼自殺吧。從我知道那刻起到仙子仍然沒有工具可處理這個問題,並且許多東西都已經更改了。藉此機會放棄你構造模塊所使用的這種方法,而後在cffi或者ctypes上從新書寫模塊。若是這種方法還不行的話,由於你有點頑固,那麼只有接受這樣的痛苦。也許試着在C預處理器上書寫某些使人討厭的事可使移植更容易些。
使用Tox來進行本地測試。可以馬上在全部Python版本上運行你的測試是很是有益的,這將爲你找到許多問題。
展望
統一2.x和3.x的基本編碼庫如今確實能夠開始了。移植的大量時間仍然將花費在試圖解決有關Unicode以及與其餘可能已經更改了自身API的模塊交互時API是如何操做上。不管如何,若是你打算考慮移植庫的話,那麼請不要觸碰2.5如下的版本、3.0-3.2版本,這樣的話將不會對版本形成太大的傷害。