1. 程式人生 > >是時候丟棄 Python 2.0,將 100 萬行的程式碼遷移到 Python 3.0 了!

是時候丟棄 Python 2.0,將 100 萬行的程式碼遷移到 Python 3.0 了!

Python 2 vs Python 3,究竟誰是效能之王?前段時間,Hackermoon 上一位叫 Anthony Shaw 的作者為我們做了一些測試,最終得出結論,雖然 Python 2 在加密和啟動時間測試過程中,比 Python 3 的速度更勝一籌,但整體而言,Python 3 更快。

而這是否就意味著我們還是將專案程式碼遷移到 Python 3.0 的好?接下來,本文來自全球著名的桌面應用之一的 Dropbox 將分享他們要棄用 Python 2.0 的真實原因,以及如何將百萬行的程式碼成功遷移至 Python 3。

640?wx_fmt=jpeg

Dropbox 是世界上流行的桌面應用之一,你可以安裝在 Windows、macOS 和部分的 Linux 發行版上。但你可能不知道,這個應用大部分是用 Python 寫的。實際上,Drew 給 Dropbox 寫下的第一行程式碼就是用的 Windows 版 Python,用的是老牌的 pywin32 等庫。

雖然我們靠著 Python 2 支撐了這麼多年(我們用過的最新版本是 Python 2.7),但我們從 2015 年就開始向 Python 3 轉換了。今天我們終於完成了轉換,你現在再裝 Dropbox 的話,那麼它用的是 Dropbox 定製版本的 Python 3.5。本文將介紹這次史無前例的 Python 3 轉換的計劃、執行和釋出過程。

640?wx_fmt=png

為什麼選擇 Python 3?

Python 3 的接受度在 Python 社群一直是熱門話題。現在雖然 Python 3 已經廣為接受(http://py3readiness.org/),一些非常流行的專案如 Django 甚至完全放棄了 Python 2 的支援,但這個話題的熱度依然存在。對於我們來說,影響我們決定進行轉換的幾個關鍵因素有:

引人入勝的新功能

Python 3 的創新十分迅速。除了一長列(http://whypy3.com/)正常的改進(如 str 和 bytes 的討論),還有幾個功能吸引了我們的眼球:

  • 型別標註語法:我們的程式碼量非常大,所以型別標註對於開發的效率非常重要。在 Dropbox 我們很喜歡 MyPy(http://mypy-lang.org/),因此原生的型別標註支援對我們很有吸引力。

  • 並行函式語法:許多功能都極度依賴執行緒和訊息傳遞,我們採用的是 Actor 模式,使用了 Future 模組。而 asyncio 專案及其 async/await 語法有時能避免回撥函式,從而獲得更乾淨的程式碼。

過老的工具鏈

隨著 Python 2 日久年深,最初適合部署的工具鏈也大部分過時了。由於這些因素,繼續使用 Python 2 會帶來一系列的維護負擔:

  • 過老的編譯器和執行時使得我無法們升級一些重要更新。

    例如,我們在 Windows 和 Linux 上使用 Qt,而最新版本的 Qt 包含了 Chromium(通過 QtWebEngine 實現),因此需要更現代的編譯器。

  • 我們與作業系統的整合越來越深,而無法使用新版本的工具鏈,導致使用新版 API 的成本增大。

    例如,理論上 Python 2 依然需要 Visual Studio 2008 (http://stevedower.id.au/blog/building-for-python-3-5/)。但這個版本微軟已經不再支援了,也與 Windows 10 SDK 不相容。

640?wx_fmt=png

凍結和指令碼

當初,我們依靠“凍結”指令碼為我們支援的每個平臺建立原生應用程式。但是,我們並沒有直接使用原生的工具鏈,如 macOS 的 Xcode,而是將建立各個平臺上的二進位制檔案的任務交給其他程式去做,Windows 下是 py2exe,macOS 下是 py2app,Linux 下是 bbfreeze。這個完全面向 Python 的構建系統收到了 distutils 的啟發,因為我們的應用最初只不過是個 Python 包,所以只需要一個類似於 setup.py 的指令碼來構建。

隨著時間的流逝,我們的程式碼量越來越大。現在,我們的開發已經不僅僅使用 Python 開發了。實際上,我們的程式碼現在由 TypeScript/HTML、Rust 和Python 混合組成,某些平臺上還用了 Objective-C 和 C++。為支援所有元件,setup.py 指令碼(內部的名字為 build-all.py)越來越大,越來越難以管理。

導火索就是我們與各個作業系統整合的方式。首先,我們越來越多地引入高階的 OS 擴充套件,如 Smart Sync 的核心元件等,這些元件不能,通常也不會使用 Python 編寫。其次,像微軟和蘋果等供應商對部署應用提出了新的需求,因此經常需要用到新的、更復雜的工具,這些工具經常是這些供應商獨有的(比如程式碼簽名等)。

例如在 macOS 上,10.10 版本引入了新的應用擴充套件以便與 Finder 進行整合,就是FinderSync(https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Finder.html)。它並不只是個 API,而是個完整的應用程式包(.appex),有自己的生存中崛起規則(即它由 OS 啟動),而且對於程序間通訊的要求更嚴格。換句話說,使用 Xcode 就很容易整合這些擴充套件,但 py2app 根本不支援它們。

因此,我們面臨著兩個問題:

  • 由於我們使用 Python 2,因此無法使用新的工具鏈,所以整合新的 API 的代價更高(比如使用Windows 10的Windows Runtime)。

  • 我們的凍結指令碼使得部署原生程式碼的代價更高(例如在 macOS 上構建應用擴充套件)。

當我們計劃轉換成 Python 3 時,我們面臨著兩個選擇:一是改進凍結指令碼中的依賴,以支援 Python 3(從而支援現代編譯器)和平臺相關的功能(如應用程式擴充套件),二是不再使用以 Python 為中心的構建系統,完全放棄凍結指令碼。我們選擇了後者。

關於 pyinstaller 的一點:我們認真地思考過在專案早期使用 pyinstaller,但當時它不支援 Python 3,而且更重要的是,它和其他凍結指令碼有類似的限制。不管怎樣,這個專案本身很不錯,我們只是覺得不適合我們而已。

640?wx_fmt=png

嵌入 Python

為了解決構建和部署的問題,我們決定使用新的架構,在原生應用中嵌入 Python 執行時。我們不再將構建過程交給凍結指令碼處理,而是使用各個平臺自己的工具鏈(比如 Windows 下使用 Visutal Studio)來構建各種入口點。進一步,我們將 Python 程式碼抽象到一個庫中,從而為多種語言“混合”的方式提供更直接的支援。

這樣我們就可以直接使用各個平臺的 IDE 和工具鏈了(例如可以直接新增原生的構建目標,如 macOS 上的 FinderSync),同時保留使用 Python 編寫大部分應用程式邏輯的能力。

我們最後採用了下面的結構:

  • 原生入口點:這些與各個平臺的應用程式模型相容。

    其中包括應用程式擴充套件,如 Windows 下的 COM 元件和 macOS 下的應用程式擴充套件。

  • 共享庫可以使用多種語言編寫(包括 Python)。

表面上,這個應用能夠更接近平臺的要求,而在各個庫的背後,我們可以有更大的靈活性來選擇自己喜歡的語言和工具。

這種架構能提高模組性,同時還帶來一個關鍵的副作用:現在可以同時部署 Python 2 庫和 Python 3 庫了。聯絡到 Python 3 轉換工作,我們的轉換過程就需要兩步:第一,給 Python 2 實現新的架構;第二,利用它將 Python 2 替換成 Python 3。

640?wx_fmt=png

第一步:“解凍”

第一步就是停止使用凍結指令碼。目前,bbfreeze 和 pywin32 都不支援 Python 3,所以我們別無選擇。我們從 2016 年開始逐步進行這項改變。

首先,我們將配置 Python 執行時的工作抽象化,將 Python 的東西放到一個新的庫中,名為 libdropbox_bootstrap。這個庫會代替一些凍結指令碼提供的功能。儘管我們不再需要這些指令碼,但它們仍然提供了一些執行 Python 程式碼所需的最基本的東西:

打包程式碼以便在裝置上執行

這樣我們才能釋出編譯好的 Python 位元組碼,而不用釋出 Python 原始碼。由於以前的每個凍結指令碼在各個平臺上有各自的格式,我們利用這個機會引入了一種新的格式,用於在所有平臺上打包程式碼使用:

  • 所有 Python 模組的 Python 的位元組碼 .pyc 都放在單一的 zip 文件中(如 python-packages-35.zip)。

  • 原生擴充套件. pyd / .so 由於是平臺相關的原生動態連結庫,他們必須安裝在特定的位置,保證應用程式能毫無障礙地載入。

    Windows 下,這些檔案與入口點(即 Dropbox.exe)放在一起。

  • 打包通過優秀的 modulegraph(作者是 py2app 和 PyObjC 的作者 Ronald Oussoren)實現。

隔離 Python 直譯器

這樣能阻止我們的應用程式在裝置上執行其他的 Python 原始碼。有意思的是,Python 3 使得這種嵌入變得容易得多了。例如,新的 Py_SetPath 函式(https://docs.python.org/3/c-api/init.html#c.Py_SetPath)能夠讓我們將程式碼隔離,不需要再像 Python 2 時代在凍結指令碼中進行某種複雜的隔離操作了。為了在 Python 2 中支援這一功能,我們在定製版本的 Python 2 中向下移植了這一功能。

其次,我們使用了平臺相關的入口點Dropbox.exe、Dropbox.app和dropboxd 來使用這個庫。這些入口點都是用各自平臺的“標準”工具編譯的,即 Visual Studio、Xcode 和 make,沒有使用 distutils。這樣我們就可以去掉凍結指令碼帶來的大量修補工作了。例如,在 Windows 下,這一步大大簡化,只需為 Dropbox.exe 配置 DEP/NX 即可,就能將應用程式裝箱單和資源嵌入了。

關於 Windows 的一點說明:現在,繼續使用 Visual Studio 2008 的代價已經非常高了。為了正確地轉換,我們需要一個能同時支援 Python 2 和 Python 3 的版本,最終我們採用了 Visual Studio 2013。為支援它,我們進一步修改了定製版本的 Python 2,使之能正確在 Visual Studio 2013 下編譯。這些修改的代價進一步證明,我們轉換到 Python 3 的決定是正確的。

640?wx_fmt=png

第二步:混合

成功地轉換如此之大(包含大約 100 萬行 Python 程式碼)、安裝量如此之高(大約有幾億安裝)的應用程式需要逐步進行。我們不能簡單地在某次釋出中“改變一個開關”來實現轉換,特別是我們的釋出過程要求每兩個星期給所有使用者釋出一個新版本。因此,必須找到一種辦法,將 Python 3 的部分轉換髮布給一小部分使用者,以便檢測並修改 Bug。

為達到這一點,我們決定實現用 Python 2 和 Python 3 同時編譯 Dropbox。這要求做到以下兩點:

  • 能夠同時釋出 Python 2 和 Python 3 的“包”,包括位元組碼和擴充套件,兩者必須能夠並存。

  • 在轉換過程中強制使用混合的 Python 2 / 3 語法。

我們採用上一步引入的嵌入式設計來實現:將 Python 程式碼抽象到庫和包中,就能很容易地引入另一個版本。這樣入口點程式(即 Dropbox.exe)就可以在初始化的早期控制選擇哪個 Python 版本了。

我們通過手動連線入口點程式到 libdropbox_bootstrap 來實現這一點。例如在 macOS 和 Linux 下,我們在 Python 版本確定之後使用 dlopen/dlsym 來載入。在 Windows 下,使用 LoadLibrary 和 GetProcAddress。

對 Python 直譯器的選擇必須在 Python 載入之前完成,因此為了使之更順暢,我們實現了命令列引數  /py3  用於開發,和一個儲存在硬碟上的永久設定,以便通過我們的功能切換系統Stormcrow(https://blogs.dropbox.com/tech/2017/03/introducing-stormcrow/)來控制。

有了這些,我們就能在啟動 Dropbox 客戶端時動態選擇 Python 版本了。這樣就可以在 CI 基礎設施中設定額外的任務來針對 Python 3 執行單元測試和整合測試。我們還在提交佇列中增加了自動檢查,以防止提交會破壞 Python 3 支援的改動。

通過自動測試確保沒問題之後,我們就開始將 Python 3 的改動推送給真正的使用者。我們通過遠端的功能開關來將新功能逐漸開放給使用者。首先對 Dropbox 推送改動,這樣我們就能找出並改正大部分主要的底層問題。然後將範圍擴大到 Beta 使用者,他們的 OS 版本問題更加蕪雜。然後最終擴充套件到穩定版。7 個月之後,所有的 Dropbox 都已經在執行 Python 3 了。為了儘可能提高質量,我們要求所有與轉換相關的 bug 必須進行深入調查並徹底修復,才能擴大推送的範圍。

640?wx_fmt=png

逐漸推送到 Beta 版

640?wx_fmt=png

逐漸推送到穩定版

到了版本 52 時,這個過程終於完成了。我們可以完全從 Dropbox 的桌面客戶端中刪掉 Python 2 了。

640?wx_fmt=png

寫在最後

一篇文章很難完整概括我們將程式碼遷移至 Python 3.0 的完整過程,這其中還有許多可以討論的東西。接下來,我們還會在以後的文章中討論:

  • 我們怎樣在 Windows 和 macOS 上報告崩潰,並利用這些資訊除錯原生和 Python 程式碼;

  • 怎樣維護 Python 2 和 Python 3 混合程式碼,用到了哪些工具?

  • 整個 Python 3 轉換過程中最值得討論的 Bug 和故事。

敬請期待,也歡迎在下方留言分享你對遷移過程的看法。

原文:https://blogs.dropbox.com/tech/2018/09/how-we-rolled-out-one-of-the-largest-python-3-migrations-ever/

--------------------- 本文來自 CSDN資訊 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/csdnnews/article/details/82888415?utm_source=copy