是否應該採用 Python 3 一直是 Python 社區爭論的焦點話題。雖然 Python 3 如今獲得了普遍的支持,一些很是受歡迎的項目(如 Django)已經徹底放棄了 Python 2,但這個爭論在必定程度上仍然存在。對於咱們來講,有一些關鍵因素影響着咱們的決定:python
使人興奮的新特性編程
Python 3 帶來了快速的創新。除了一長串通常性改進以外,一些特別的特性引發了咱們的注意:bootstrap
類型註解語法:咱們的代碼庫很是大,所以使用類型註解對於提高開發人員的工做效率來講是很是重要的。咱們是 MyPy 的忠實粉絲,所以原生支持類型註解天然會吸引到咱們。架構
協程函數語法:咱們嚴重依賴線程模型和消息傳遞來開發咱們的不少功能。asyncio 項目及其 async/await 語法有時能夠消除對回調的需求,從而讓代碼變得更清晰。app
老舊的工具鏈async
隨着 Python 2 變得老舊,相應的工具鏈在很大程度上也已通過時了。所以,繼續使用 Python 2 將伴隨着日益增長的維護負擔:編程語言
使用舊編譯器 / 運行時限制了咱們升級某些重要依賴項的能力。例如,咱們在 Windows 和 Linux 上使用 Qt:因爲包含了 Chromium(經過 QtWebEngine),最新版本的 Qt 須要更現代的編譯器。ide
隨着繼續深刻與操做系統集成,咱們沒法依賴這些工具鏈的更新版本,所以增長了採用新 API 的成本。例如,Python 2 仍然須要 Visual Studio 2008,而微軟再也不支持這個版本,而且與 Windows 10 SDK 不兼容。模塊化
freezer 和腳本函數
最初,咱們依靠「freezer」腳本爲各個平臺建立原生應用程序。可是,咱們不是直接使用原生工具鏈(例如在 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)變得龐大而混亂,難以維護。
轉折點來自於咱們與每一個操做系統集成的轉變:首先,咱們開始逐步引入愈來愈先進的操做系統擴展(如 Smart Sync 內核組件),這些擴展一般不是使用 Python 編寫的。其次,微軟和蘋果開始強制使用更復雜的新工具(一般是專有工具,例如代碼簽名)來部署應用程序。
例如,macOS 10.10 引入了一個新的應用程序擴展 FinderSync,用於與 Finder 集成。FinderSync 擴展不只僅是一個 API,它仍是一個完整的應用程序包(.appex),它具備自定義生命週期規則(由操做系統啓動)以及對進程間通訊更嚴格的要求。經過 Xcode 能夠很容易地利用這些擴展,但 py2app 並不徹底支持它們。
所以,咱們面臨兩個問題:
Python 2 對使用新的工具鍊形成阻礙,提高了使用新 API 的成本(例如在 Windows 10 上使用 Windows 運行時)。
freezer 腳本也提高了部署原生代碼的成本(例如在 macOS 上構建應用程序擴展)。
要遷移到 Python 3,咱們須要作出選擇:修改 freezer 依賴項,增長對 Python 3(以及現代編譯器)和平臺特定特性(如 app 擴展)的支持,或者拋棄以 Python 爲中心的構建系統,完全廢除「freezer」。咱們選擇了後者。
至於 pyinstaller,咱們在項目早期考慮過使用它,但它當時不支持 Python 3,更重要的是,它與 freezer 同樣也存在相似的限制。它是個好東西,只是不符合咱們的要求。
嵌入 Python
爲了解決這個構建和部署問題,咱們決定將 Python 運行時嵌入到原生應用程序中。咱們沒有將這個過程委託給 freezer,而是使用每一個平臺特定的工具(例如 Windows 上的 Visual Studio)來構建各類入口點。此外,咱們將 Python 代碼代碼抽離成一個庫,以便更直接地支持與其餘語言的「混合和匹配」。
這樣咱們就能夠直接使用每一個平臺的 IDE 和工具鏈(例如在 macOS 上添加 FinderSync),同時仍然可使用 Python 編寫應用程序邏輯。
咱們採用瞭如下結構:
原生入口點:這些入口點與每一個平臺的應用程序模型兼容,包括應用程序擴展,例如 Windows 上的 COM 組件或 macOS 上的 app 擴展。
使用多種語言(包括 Python)編寫共享庫。
從表面上看,應用程序將更加相似於平臺所指望的,但在各類庫背後,咱們的團隊能夠更靈活地使用他們喜歡的編程語言或工具。
這種架構所帶來的模塊化能力還產生了一個關鍵的反作用:如今能夠同時部署 Python 2 和 Python 3 庫。在 Python 3 遷移中使用這種方法須要兩個步驟:第一,圍繞 Python 2 實現新架構,第二,使用 Python 3 代替 Python 2。
第 1 步:「Anti-freeze」
咱們的第一步是中止使用 freezer 腳本。bbfreeze 和 pywin32 都缺少對 Python 3 的支持,因此咱們別無選擇。從 2016 年開始,咱們開始逐步作出這一改變。
首先,咱們將配置 Python 運行時和啓動 Python 線程的工做抽離到一個叫做 libdropbox_bootstrap 的庫中。這個庫能夠完成以前由 freezer 腳本完成的一些工做。雖然咱們在很大程度上已經再也不須要依賴這些腳本,但仍然須要爲運行 Python 代碼的提供一些基本的東西:
打包代碼,以便在設備上運行
這須要確保咱們提供的是通過編譯的 Python「字節碼」,而不是 Python 源代碼。以前,每一個 freezer 腳本都有本身的存儲格式,咱們借這個機會引入了一種單一的格式,用於在全部平臺上打包咱們的代碼:
對於 Python 字節碼.pyc,單個 ZIP 壓縮包(例如 python-packages-35.zip)包含了全部必需的 Python 模塊。
對於原生擴展.pyd/.so,它們是平臺原生 DLL,被安裝在一個特定位置,以確保應用程序能夠加載到它們。例如,在 Windows 上,它們與入口點(即 Dropbox.exe)放在一塊兒。
使用 modulegraph 來打包。
隔離 Python 解釋器
這樣能夠防止應用程序執行設備上的其餘 Python 代碼。有趣的是,Python 3 讓這種類型的嵌入變得更加容易。例如,藉助新的 Py_SetPath 函數,咱們能夠很容易地隔離代碼,避免了在 Python 2 中隔離 freezer 腳本須要作的那些比較複雜的工做。爲了可以在 Python 2 中進行隔離,咱們將這個函數反向移植到自定義的分支代碼庫中。
其次,咱們引入了特定於各個平臺的入口點,如 Dropbox.exe、Dropbox.app 和 dropboxd,並讓這些入口點使用這個庫。這些入口點是使用每一個平臺的「標準」工具構建的:Visual Studio、Xcode 和 make,這樣咱們就能夠刪除 freezer 腳本中的大部分自定義拼湊代碼。例如,在 Windows 上,這極大地簡化了爲 Dropbox.exe 配置 DEP/NX 以及嵌入應用程序清單和包含資源文件。
關於 Windows 的說明:在這個時間點上,繼續使用 Visual Studio 2008 的成本變得很是高。咱們須要一個可以同時支持 Python 2 和 Python 3 的版本,因而咱們選擇了 Visual Studio 2013。爲了支持它,咱們對 Python 2 的自定義分支進行了大量修改,以便可以正常編譯。爲這些變化所作出的努力進一步加強了咱們的信念:轉向 Python 3 是正確的決定。
第 2 步:Hydra
進行這麼大規模的轉換(咱們的應用程序包含超過 100 萬個 Python LOC,並被安裝超過數億次)是一個漸進的過程:咱們不能簡單粗暴地在一個版本中搞定一切。固然,這個與咱們的發佈流程也有關係,咱們每兩週向全部用戶發佈一個新版本。咱們須要找到一種方法,讓少許用戶先用上 Python 3,便於及早發現和修復 bug。
爲實現這一目標,咱們決定同時使用 Python 2 和 Python 3 來構建 Dropbox。這要求:
可以同時提供 Python 2 和 Python 3「軟件包」,以及字節碼和擴展。
在轉換期間強制混合使用 Python 2 和 Python 3 語法。
咱們利用了前一個步驟引入的嵌入式設計:將 Python 抽離爲單獨的庫,能夠很容易地引入另外一個版本的變體。而後,在入口點(例如 Dropbox.app)初始化期間,能夠選擇要使用的 Python 版本。
這是經過手動將入口點連接到 libdropbox_bootstrap 來實現的。例如,在 macOS 和 Linux 上,在選定了 Python 版本以後,咱們就使用 dlopen/dlsym。在 Windows 上,咱們使用 LoadLibrary 和 GetProcAddress。
在加載 Python 以前須要選擇運行時 Python 解釋器,在開發時使用命令行參數 /py3,在實際部署時使用磁盤文件來設置,這樣就能夠經過咱們的功能門控系統 Stormcrow 來控制它。
所以,咱們在啓動 Dropbox 客戶端時可以動態地選擇 Python 版本。咱們也所以可以在 CI 基礎設施上設置額外的做業來運行鍼對 Python 3 的單元測試和集成測試。咱們還對代碼提交隊列進行自動檢查,防止某些代碼變動出現回退。
在經過自動化測試得到了足夠的信心以後,咱們開始向真實用戶推出基於 Python 3 的版本。咱們經過遠程功能門控系統來逐步向用戶推出新客戶端。咱們先是將新版本推給 Dropboxer,這樣就能找出並修復大多數潛在的問題。而後,咱們向一小部分用戶推出 Beta 版本,並最終擴展到了穩定版渠道:在 7 個月內,全部 Dropbox 用戶都開始使用 Python 3 版本。爲了最大限度地提升質量,咱們制定了一個策略,在向更大的用戶羣推出新版本以前,必須修復全部與遷移相關的問題。
直到版本 52,咱們才完成了整個遷移過程:Python 2 已經從 Dropbox 的桌面客戶端中完全移除。