將 Python 2 代碼遷移到 Python 3?
- 作者
Brett Cannon
摘要
Python 3 是 Python 的未來(lái),但 Python 2 仍處于活躍使用階段,最好讓您的項目在兩個(gè)主要版本的Python 上都可用。本指南旨在幫助您了解如何最好地同時(shí)支持 Python 2 和 3。
如果您希望遷移擴展模塊而不是純 Python 代碼,請參閱 將擴展模塊移植到 Python 3。
如果你想了解核心 Python 開(kāi)發(fā)者對于 Python 3 的出現有何看法,你可以閱讀 Nick Coghlan 的 Python 3 Q & A 或 Brett Cannon 的 為什么要有 Python 3.
關(guān)于遷移的幫助,你可以查看存檔的 python-porting 郵件列表。
簡(jiǎn)要說(shuō)明?
為了使你的項目以一份源代碼與Python 2/3兼容,基本步驟如下:
只擔心支持Python 2.7的問(wèn)題
確保你有良好的測試覆蓋率(可以用 coverage.py;
python -m pip install coverage
)。了解Python 2 和 3之間的區別
使用 Futurize (或Modernize) 來(lái)更新你的代碼 (例如``python -m pip install future``)。
使用 Pylint 來(lái)幫助確保你在Python 3支持上不倒退(
python -m pip install pylint
)使用 caniusepython3 來(lái)找出你的哪些依賴(lài)關(guān)系阻礙了你對 Python 3 的使用 (
python -m pip install caniusepython3
)一旦你的依賴(lài)性不再阻礙你,使用持續集成來(lái)確保你與 Python 2 和 3 保持兼容 (tox 可以幫助對多個(gè)版本的 Python 進(jìn)行測試;
python -m pip install tox
)考慮使用可選的靜態(tài)類(lèi)型檢查,以確保你的類(lèi)型用法在 Python 2 和 3 中都適用 (例如,使用 mypy 來(lái)檢查你在 Python 2 和 Python 3 中的類(lèi)型;
python -m pip install mypy
)。
備注
注意:使用 python -m pip install
來(lái)確保你發(fā)起調用的 pip
就是當前使用的 Python 所安裝的那一個(gè),無(wú)論它是系統級的 pip
還是安裝在 虛擬環(huán)境 中的。
詳情?
同時(shí)支持Python 2和3的一個(gè)關(guān)鍵點(diǎn)是,你可以從**今天**開(kāi)始!即使你的依賴(lài)關(guān)系還不支持Python 3,也不意味著(zhù)你不能現在就對你的代碼進(jìn)行現代化改造以支持Python 3。支持Python 3所需的大多數變化都會(huì )使代碼更干凈,甚至在Python 2的代碼中也會(huì )使用更新的做法。
另一個(gè)關(guān)鍵點(diǎn)是,使你的Python 2代碼現代化以支持Python 3在很大程度上是為你自動(dòng)化的。雖然由于Python 3清晰區分了文本數據與二進(jìn)制數據,你可能必須做出一些API決定。但現在已經(jīng)為你完成了大部分底層的工作,因此至少可以立即從自動(dòng)化變化中受益。
當你繼續閱讀關(guān)于遷移你的代碼以同時(shí)支持Python 2和3的細節時(shí),請牢記這些關(guān)鍵點(diǎn)。
刪除對Python 2.6及更早版本的支持?
雖然你可以讓 Python 2.5 與 Python 3 一起工作,但如果你只需要與 Python 2.7 一起工作,那就會(huì ) 更加容易。 如果放棄 Python 2.5 不是一種選擇,那么 six 項目可以幫助你同時(shí)支持 Python 2.5 和 3 (python -m pip install six
)。 不過(guò)請注意,本 HOWTO 中列出的幾乎所有項目都有可能會(huì )不再可用。
如果你能夠跳過(guò)Python 2.5和更早的版本,那么對你的代碼所做的必要修改應該看起來(lái)和感覺(jué)上都是你已經(jīng)習慣的Python代碼。在最壞的情況下,你將不得不使用一個(gè)函數而不是一個(gè)實(shí)例方法,或者不得不導入一個(gè)函數而不是使用一個(gè)內置的函數,但除此之外,整體的轉變應該不會(huì )讓你感到陌生。
但你的目標應該是只支持 Python 2.7。Python 2.6 不再被免費支持,因此也就沒(méi)有得到錯誤修復。這意味著(zhù)**你**必須解決你在Python 2.6上遇到的任何問(wèn)題。本HOWTO中提到的一些工具也不支持Python 2.6 (例如,Pylint),隨著(zhù)時(shí)間的推移,這將變得越來(lái)越普遍。如果你只支持你必須支持的Python版本,那么對你來(lái)說(shuō)會(huì )更容易。
確保你在你的``setup.py``文件中指定適當的版本支持?
在你的``setup.py``文件中,你應該有適當的 trove classifier 指定你支持哪些版本的 Python。由于你的項目還不支持 Python 3,你至少應該指定``Programming Language :: Python :: 2 :: Only``。理想情況下,你還應該指定你所支持的Python的每個(gè)主要/次要版本,例如:Programming Language :: Python :: 2.7
。.
良好的測試覆蓋率?
一旦你的代碼支持了你希望的Python 2的最老版本,你將希望確保你的測試套件有良好的覆蓋率。一個(gè)好的經(jīng)驗法則是,如果你想對你的測試套件有足夠的信心,在讓工具重寫(xiě)你的代碼后出現的任何故障都是工具中的實(shí)際錯誤,而不是你的代碼中的錯誤。如果你想要一個(gè)目標數字,試著(zhù)獲得超過(guò)80%的覆蓋率(如果你發(fā)現很難獲得好于90%的覆蓋率,也不要感到遺憾)。如果你還沒(méi)有一個(gè)測量測試覆蓋率的工具,那么推薦使用coverage.py。
了解Python 2 和 3之間的區別?
一旦你的代碼經(jīng)過(guò)了很好的測試,你就可以開(kāi)始把你的代碼移植到 Python 3 上了! 但是為了充分了解你的代碼將發(fā)生怎樣的變化,以及你在編碼時(shí)要注意什么,你將會(huì )想要了解 Python 3 相比 Python 2 的變化。 通常來(lái)說(shuō),兩個(gè)最好的方法是閱讀 Python 3 每個(gè)版本的 "What's New" 文檔和 Porting to Python 3 書(shū)(在線(xiàn)免費版本)。還有一個(gè)來(lái)自 Python-Future 項目的便利 cheat sheet。
更新代碼?
一旦你覺(jué)得你知道了 Python 3 與 Python 2 相比有什么不同,就是時(shí)候更新你的代碼了! 你可以選擇兩種工具來(lái)自動(dòng)移植你的代碼: Futurize 和 Modernize。 你選擇哪個(gè)工具將取決于你希望你的代碼有多像 Python 3。 Futurize 盡力使 Python 3 的習性和做法在 Python 2 中存在,例如,從 Python 3 中回傳 bytes
類(lèi)型,這樣你就可以在 Python 的主要版本之間實(shí)現語(yǔ)義上的平等。 另一方面,Modernize 更加保守,它針對的是 Python 2/3 的子集,直接依靠 six 來(lái)幫助提供兼容性。 由于 Python 3 是未來(lái),最好考慮 Futurize,以開(kāi)始適應 Python 3 所引入的、你還不習慣的任何新做法。
無(wú)論你選擇哪種工具,它們都會(huì )更新你的代碼,使之在Python 3下運行,同時(shí)與你開(kāi)始使用的Python 2版本保持兼容。根據你想要的保守程度,你可能想先在你的測試套件上運行該工具,并目測差異以確保轉換的準確性。在你轉換了你的測試套件并驗證了所有的測試仍然如期通過(guò)后,你就可以轉換你的應用程序代碼了,因為你知道任何測試失敗都是轉譯的錯誤。
不幸的是,這些工具不能自動(dòng)地使你的代碼在Python 3下工作,因此有一些東西你需要手動(dòng)更新以獲得對Python 3的完全支持(這些步驟在不同的工具中是必要的)。閱讀你選擇使用的工具的文檔,看看它默認修復了什么,以及它可以選擇做什么,以了解哪些將(不)為你修復,哪些你可能必須自己修復(例如,在Modernize中使用``io.open()``覆寫(xiě)內置的``open()``函數默認是關(guān)閉的)。不過(guò)幸運的是,只有幾件需要注意的事情,可以說(shuō)是大問(wèn)題,如果不注意的話(huà),可能很難調試。
除法?
在Python 3中,5 / 2 == 2.5``而不是``2
;所有``int``數值之間的除法都會(huì )產(chǎn)生一個(gè)``float``、這個(gè)變化實(shí)際上從2002年發(fā)布的Python 2.2開(kāi)始就已經(jīng)計劃好了。從那時(shí)起,我們就鼓勵用戶(hù)在所有使用``/和
//運算符的文件中添加``from __future__ import division
,或者在運行解釋器時(shí)使用``-Q``標志。如果你沒(méi)有這樣做,那么你需要檢查你的代碼并做兩件事。
添加
from __future__ import division
到你的文件。根據需要更新任何除法運算符,要么使用
//
來(lái)使用向下取整除法,要么繼續使用/
并得到一個(gè)浮點(diǎn)數
之所以沒(méi)有簡(jiǎn)單地將 /
自動(dòng)翻譯成 //
,是因為如果一個(gè)對象定義了一個(gè) __truediv__
方法,但沒(méi)有定義 __floordiv__
,那么你的代碼就會(huì )運行失?。ɡ?,一個(gè)用戶(hù)定義的類(lèi)用 /
來(lái)表示一些操作,但沒(méi)有用 //
來(lái)表示同樣的事情或根本沒(méi)有定義)。
文本與二進(jìn)制數據?
在Python 2中,你可以對文本和二進(jìn)制數據都使用``str``類(lèi)型。不幸的是,這兩個(gè)不同概念的融合可能會(huì )導致脆弱的代碼,有時(shí)對任何一種數據都有效,有時(shí)則無(wú)效。如果人們沒(méi)有明確說(shuō)明某種接受``str``東西可以接受文本或二進(jìn)制數據,而不是一種特定的類(lèi)型,這也會(huì )導致API的混亂。這使情況變得復雜,特別是對于任何支持多種語(yǔ)言的人來(lái)說(shuō),因為API在聲稱(chēng)支持文本數據時(shí)不會(huì )顯式支持``unicode``。
為了使文本和二進(jìn)制數據之間的區別更加清晰和明顯,Python 3做了大多數在互聯(lián)網(wǎng)時(shí)代創(chuàng )建的語(yǔ)言所做的事情,使文本和二進(jìn)制數據成為不能盲目混合在一起的不同類(lèi)型(Python早于互聯(lián)網(wǎng)的廣泛使用)。對于任何只處理文本或只處理二進(jìn)制數據的代碼,這種分離并不構成問(wèn)題。但是對于必須處理這兩種數據的代碼來(lái)說(shuō),這確實(shí)意味著(zhù)你現在可能必須關(guān)心你何時(shí)使用文本或二進(jìn)制數據,這就是為什么這不能完全自動(dòng)化遷移。
首先,你需要決定哪些 API 接受文本,哪些接受二進(jìn)制(由于保持代碼工作的難度,強烈 建議你不要設計同時(shí)接受兩種數據的 API;如前所述,這很難做得好)。在 Python 2 中,這意味著(zhù)要確保處理文本的 API 能夠與 unicode
一起工作,而處理二進(jìn)制數據的 API 能夠與 Python 3 中的 bytes
類(lèi)型一起工作(在 Python 2 中是 str
的一個(gè)子集,在 Python 2 中作為 bytes
類(lèi)型的別名)。通常最大的問(wèn)題是意識到哪些方法同時(shí)存在于 Python 2 和 3 的哪些類(lèi)型上 (對于文本來(lái)說(shuō),Python 2 中是 unicode
,Python 3 中是 str
,對于二進(jìn)制來(lái)說(shuō),Python 2 中是 str
/bytes
,Python 3 中是``bytes``)。 下表列出了每個(gè)數據類(lèi)型在 Python 2 和 3 中的 獨特 的方法 (例如,decode()
方法在 Python 2 或 3 中的等價(jià)二進(jìn)制數據類(lèi)型上是可用的,但是它不能在 Python 2 和 3 之間被文本數據類(lèi)型一致使用,因為 Python 3 中的 str
沒(méi)有這個(gè)方法)。 請注意,從 Python 3.5 開(kāi)始,__mod__
方法被添加到 bytes 類(lèi)型中。
文本數據 |
二進(jìn)制數據 |
decode |
|
encode |
|
format |
|
isdecimal |
|
isnumeric |
通過(guò)在你的代碼邊緣對二進(jìn)制數據和文本進(jìn)行編碼和解碼,可以使這種區分更容易處理。這意味著(zhù),當你收到二進(jìn)制數據的文本時(shí),你應該立即對其進(jìn)行解碼。而如果你的代碼需要將文本作為二進(jìn)制數據發(fā)送,那么就盡可能晚地對其進(jìn)行編碼。這使得你的代碼在內部只與文本打交道,從而不必再去跟蹤你所處理的數據類(lèi)型。
下一個(gè)問(wèn)題是確保你知道你的代碼中的字符串字頭是代表文本還是二進(jìn)制數據。你應該給任何呈現二進(jìn)制數據的字面符號添加一個(gè)``b``前綴。對于文本,你應該給文本字面添加一個(gè)``u``前綴。(有一個(gè) __future__
導入來(lái)強制所有未指定的字頭為Unicode,但實(shí)際使用情況表明它并不像給所有字頭顯式添加一個(gè)``b``或``u``前綴那樣有效)
作為這種二分法的一部分,你還需要小心打開(kāi)文件。除非你一直在 Windows 上工作,否則你有可能在打開(kāi)二進(jìn)制文件時(shí)沒(méi)有一直費心地添加``b``模式 (例如,用``rb``進(jìn)行二進(jìn)制讀?。?。 在 Python 3 下,二進(jìn)制文件和文本文件顯然是不同的,而且是相互不兼容的;詳見(jiàn) io
模塊。因此,你必須**決定**一個(gè)文件是用于二進(jìn)制訪(fǎng)問(wèn)(允許讀取和/或寫(xiě)入二進(jìn)制數據)還是文本訪(fǎng)問(wèn)(允許讀取和/或寫(xiě)入文本數據)。你還應該使用 io.open()
來(lái)打開(kāi)文件,而不是內置的 open()
函數,因為 io
模塊從 Python 2 到 3 是一致的,而內置的 open()
函數則不是 (在 Python 3 中它實(shí)際上是 io.open()
)。不要理會(huì )使用 codecs.open()
的過(guò)時(shí)做法,因為這只是為了保持與 Python 2.5 的兼容性。
在Python 2和3中,str``和``bytes``的構造函數對相同的參數有不同的語(yǔ)義。在Python 2中,傳遞一個(gè)整數給``bytes
,你將得到整數的字符串表示:bytes(3) == '3'
。但是在Python 3中,一個(gè)整數參數傳遞給``bytes``將給你一個(gè)與指定的整數一樣長(cháng)的bytes對象,其中充滿(mǎn)了空字節:bytes(3) == b'\x00\x00\x00'
。當把bytes對象傳給``str``時(shí),類(lèi)似的擔心是必要的。 在Python 2中,你只是又得到了該bytes對象:str(b'3') == b'3'
。但是在Python 3中,你得到bytes對象的字符串表示:str(b'3') == "b'3'"
。
最后,二進(jìn)制數據的索引需要仔細處理(切片 不需要 任何特殊處理)。在 Python 2 中 b'123'[1] == b'2'
,而在 Python 3 中``b'123'[1] == 50``。 因為二進(jìn)制數據只是二進(jìn)制數的集合,Python 3 會(huì )返回你索引的字節的整數值。 但是在 Python 2 中,因為 bytes == str
,索引會(huì )返回一個(gè)單項的字節片斷。 six 項目有一個(gè)名為 six.indexbytes()
的函數,它將像在 Python 3 中一樣返回一個(gè)整數: six.indexbytes(b'123', 1)
。
總結一下:
決定你的API中哪些采用文本,哪些采用二進(jìn)制數據
確保你對文本工作的代碼也能對``unicode``工作,對二進(jìn)制數據的代碼在Python 2中能對``bytes``工作(關(guān)于每種類(lèi)型不能使用的方法,見(jiàn)上表)。
用``b``前綴標記所有二進(jìn)制字詞,用``u``前綴標記文本字詞
盡快將二進(jìn)制數據解碼為文本,盡可能晚地將文本編碼為二進(jìn)制數據
使用
io.open()
打開(kāi)文件,并確保在適當時(shí)候指定b
模式。在對二進(jìn)制數據進(jìn)行索引時(shí)要小心
使用特征檢測而不是版本檢測?
你不可避免地會(huì )有一些代碼需要根據運行的 Python 版本來(lái)選擇要做什么。做到這一點(diǎn)的最好方法是對你運行的 Python 版本是否支持你所需要的東西進(jìn)行特征檢測。如果由于某種原因這不起作用,那么你應該讓版本檢測針對 Python 2 而不是 Python 3。為了幫助解釋這個(gè)問(wèn)題,讓我們看一個(gè)例子。
假設你需要訪(fǎng)問(wèn) importlib
的一個(gè)功能,該功能自Python 3.3開(kāi)始在Python的標準庫中提供,并且通過(guò)PyPI上的 importlib2 提供給Python 2。你可能會(huì )想寫(xiě)代碼來(lái)訪(fǎng)問(wèn)例如 importlib.abc
模塊,方法如下:
import sys
if sys.version_info[0] == 3:
from importlib import abc
else:
from importlib2 import abc
這段代碼的問(wèn)題是,當Python 4出來(lái)的時(shí)候會(huì )發(fā)生什么?最好是將Python 2作為例外情況,而不是Python 3,并假設未來(lái)的Python版本與Python 3的兼容性比Python 2更強:
import sys
if sys.version_info[0] > 2:
from importlib import abc
else:
from importlib2 import abc
不過(guò),最好的解決辦法是根本不做版本檢測,而是依靠特征檢測。這就避免了任何潛在的版本檢測錯誤的問(wèn)題,并有助于保持你對未來(lái)的兼容:
try:
from importlib import abc
except ImportError:
from importlib2 import abc
防止兼容性退步?
一旦你完全翻譯了你的代碼,使之與 Python 3 兼容,你將希望確保你的代碼不會(huì )退步,不會(huì )在 Python 3上停止工作。如果你有一個(gè)依賴(lài)關(guān)系阻礙了你目前在Python 3上的實(shí)際運行,那就更是如此了。
為了幫助保持兼容,你創(chuàng )建的任何新模塊都應該在其頂部至少有以下代碼塊:
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
你也可以在運行Python 2時(shí)使用``-3``標志,對你的代碼在執行過(guò)程中引發(fā)的各種兼容性問(wèn)題進(jìn)行警告。如果你用``-Werror``把警告變成錯誤,那么你可以確保你不會(huì )意外地錯過(guò)一個(gè)警告。
你也可以使用 Pylint 項目和它的``--py3k``標志來(lái)提示你的代碼,當你的代碼開(kāi)始偏離 Python 3 的兼容性時(shí),就會(huì )收到警告。這也避免了你不得不定期在你的代碼上運行 Modernize 或 Futurize 來(lái)捕捉兼容性的退步。這確實(shí)要求你只支持Python 2.7和Python 3.4或更新的版本,因為這是Pylint支持的最小Python版本。
檢查哪些依賴(lài)性會(huì )阻礙你的過(guò)渡?
在你使你的代碼與 Python 3 兼容**之后**,你應該開(kāi)始關(guān)心你的依賴(lài)關(guān)系是否也被移植了。 caniusepython3 項目的建立是為了幫助你確定哪些項目——直接或間接地——阻礙了你對Python 3的支持。它既有一個(gè)命令行工具,也有一個(gè)在 https://caniusepython3.com 的網(wǎng)頁(yè)界面。
該項目還提供了一些代碼,你可以將其集成到你的測試套件中,這樣,當你不再有依賴(lài)關(guān)系阻礙你使用Python 3時(shí),你將有一個(gè)失敗的測試。這使你不必手動(dòng)檢查你的依賴(lài)性,并在你可以開(kāi)始在Python 3上運行時(shí)迅速得到通知。
更新你的``setup.py``文件以表示對Python 3的兼容?
一旦你的代碼在 Python 3 下工作,你應該更新你``setup.py``中的分類(lèi)器,使其包含``Programming Language :: Python :: 3``并不指定單獨的 Python 2 支持。這將告訴使用你的代碼的人,你支持Python 2 和 3。理想情況下,你也希望為你現在支持的Python的每個(gè)主要/次要版本添加分類(lèi)器。
使用持續集成以保持兼容?
一旦你能夠完全在Python 3下運行,你將希望確保你的代碼總是在Python 2和3下工作。在多個(gè)Python解釋器下運行測試的最好工具可能是 tox 。然后你可以將 tox 與你的持續集成系統集成,這樣你就不會(huì )意外地破壞對 Python 2 或 3 的支持。
你可能還想在 Python 3 解釋器中使用``-bb``標志,以便在你將bytes與string或bytes與int進(jìn)行比較時(shí)觸發(fā)一個(gè)異常(后者從 Python 3.5 開(kāi)始可用)。默認情況下,類(lèi)型不同的比較只是簡(jiǎn)單地返回``False``,但是如果你在文本/二進(jìn)制數據處理或字節的索引分離中犯了一個(gè)錯誤,你就不容易發(fā)現這個(gè)錯誤。當這些類(lèi)型的比較發(fā)生時(shí),這個(gè)標志會(huì )觸發(fā)一個(gè)異常,使錯誤更容易被發(fā)現。
基本上就是這樣了! 在這一點(diǎn)上,你的代碼庫同時(shí)與 Python 2 和 3 兼容。你的測試也將被設置為不會(huì )意外地破壞 Python 2 或 3 的兼容性,無(wú)論你在開(kāi)發(fā)時(shí)通常在哪個(gè)版本下運行測試。
考慮使用可選的靜態(tài)類(lèi)型檢查?
另一個(gè)幫助移植你的代碼的方法是在你的代碼上使用靜態(tài)類(lèi)型檢查器,如 mypy 或 pytype。這些工具可以用來(lái)分析你的代碼,就像它在Python 2下運行一樣,然后你可以第二次運行這個(gè)工具,就像你的代碼在Python 3下運行一樣。通過(guò)像這樣兩次運行靜態(tài)類(lèi)型檢查器,你可以發(fā)現你是否錯誤地使用了二進(jìn)制數據類(lèi)型,例如在Python的一個(gè)版本中與另一個(gè)版本相比。如果你在你的代碼中添加了可選的類(lèi)型提示,你也可以明確說(shuō)明你的API是使用文本數據還是二進(jìn)制數據,這有助于確保在兩個(gè)版本的Python中所有的功能都符合預期。