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