對 4 萬行 Python 代碼進行類型檢查的路徑。 第2部分

今天,我們將發布有關 Dropbox 如何組織數百萬行 Python 程式碼的類型控制的材料翻譯的第二部分。

對 4 萬行 Python 代碼進行類型檢查的路徑。 第2部分

閱讀第一部分

官方類型支援 (PEP 484)

2014 年 Hack Week 期間,我們在 Dropbox 上使用 mypy 進行了第一次認真的實驗。Hack Week 是由 Dropbox 主辦的為期一周的活動。 在此期間,員工可以做任何他們想做的事情! Dropbox 一些最著名的技術項目就是在此類活動中開始的。 透過這次實驗,我們得出的結論是 mypy 看起來很有前途,儘管該專案尚未準備好廣泛使用。

當時,標準化 Python 類型提示系統的想法正在醞釀中。 正如我所說,自 Python 3.0 以來,可以對函數使用類型註釋,但這些只是任意表達式,沒有定義的語法和語義。 在程式執行期間​​,這些註釋在很大程度上被忽略。 Hack Week 之後,我們開始致力於語意標準化。 這項工作導致了出現 人教版484 (Guido van Rossum、Łukasz Langa 和我合作編寫了這份文件)。

我們的動機可以從兩個方面來看。 首先,我們希望整個Python生態系統能夠採用一種通用的方法來使用類型提示(Python中使用的術語,相當於「類型註釋」)。 考慮到可能的風險,這比使用許多相互不相容的方法要好。 其次,我們想與 Python 社群的許多成員公開討論型別註釋機制。 這種願望部分是因為我們不想在廣大 Python 程式設計師眼中看起來像是「背棄了該語言的基本思想」。 它是一種動態類型語言,稱為“鴨子類型”。 在社群中,一開始,對靜態類型的想法不免產生了一些懷疑的態度。 但在明確靜態類型不再是強制性的(並且在人們意識到它實際上有用之後),這種情緒最終減弱了。

最終採用的類型提示語法與 mypy 當時支援的非常相似。 PEP 484 於 3.5 年隨 Python 2015 一起發布。 Python 不再是一種動態型別語言。 我喜歡將此事件視為 Python 歷史上的一個重要里程碑。

開始遷移

2015 年底,Dropbox 創建了一個三人團隊來開發 mypy。 他們包括吉多·範羅蘇姆、格雷格·普萊斯和大衛·費舍爾。 從那一刻起,事態開始迅速發展。 mypy 成長的第一個障礙是效能。 正如我上面所暗示的,在專案的早期,我考慮將 mypy 實作翻譯成 C,但這個想法現在被劃掉了。 我們一直堅持使用 CPython 解釋器運行系統,這對於 mypy 等工具來說不夠快。 (PyPy 專案是一個帶有 JIT 編譯器的替代 Python 實現,也沒有幫助我們。)

幸運的是,一些演算法的改進在這裡為我們提供了幫助。 第一個強大的「加速器」是增量檢查的實施。 這項改進背後的想法很簡單:如果自上次運行 mypy 以來所有模組的依賴項都沒有更改,那麼我們可以在處理依賴項時使用上次運行期間快取的資料。 我們只需要對修改的檔案和依賴它們的檔案執行類型檢查。 Mypy 甚至更進一步:如果一個模組的外部介面沒有改變,mypy 就認為導入這個模組的其他模組不需要再檢查。

在註解大量現有程式碼時,增量檢查對我們有很大幫助。 關鍵是,這個過程通常涉及 mypy 的多次迭代運行,因為註釋會逐漸添加到程式碼中並逐漸改進。 mypy 的第一次運行仍然非常緩慢,因為它有很多依賴項需要檢查。 然後,為了改善這種情況,我們實作了遠端快取機制。 如果 mypy 偵測到本機快取可能已過期,它會從集中儲存庫下載整個程式碼庫的目前快取快照。 然後,它使用此快照執行增量檢查。 這使我們在提高 mypy 性能方面又邁出了一大步。

這是 Dropbox 快速、自然地採用類型檢查的時期。 到 2016 年底,我們已經擁有大約 420000 行帶有型別註解的 Python 程式碼。 許多用戶熱衷於類型檢查。 越來越多的開發團隊正在使用 Dropbox mypy。

那時一切看起來都很好,但我們還有很多事情要做。 我們開始定期進行內部用戶調查,以找出專案的問題領域並了解哪些問題需要先解決(這種做法至今仍在公司使用)。 很明顯,最重要的是兩項任務。 首先,我們需要更多的程式碼類型覆蓋率,其次,我們需要 mypy 更快工作。 很明顯,我們加速 mypy 並將其實施到公司專案中的工作還遠未完成。 我們充分認識到這兩項任務的重要性,並著手解決它們。

更高的生產力!

增量檢查使 mypy 更快,但該工具仍然不夠快。 許多增量檢查持續約一分鐘。 其原因是周期性進口。 這可能不會讓任何使用過用 Python 編寫的大型程式碼庫的人感到驚訝。 我們有數百個模組,每個模組間接導入所有其他模組。 如果導入循環中的任何文件發生更改,mypy 必須處理該循環中的所有文件,通常也會處理從該循環導入模組的任何模組。 其中一個循環是臭名昭著的“依賴關係纏結”,它給 Dropbox 帶來了很多麻煩。 一旦這個結構包含數百個模組,在直接或間接導入許多測試的同時,它也被用於生產程式碼中。

我們考慮了「解開」循環依賴的可能性,但我們沒有資源來做到這一點。 有太多我們不熟悉的程式碼。 結果,我們想出了一種替代方法。 我們決定讓 mypy 快速工作,即使存在「依賴關係纏結」。 我們使用 mypy 守護程式實現了這一目標。 守護進程是一個伺服器進程,它實作了兩個有趣的功能。 首先,它將整個程式碼庫的資訊儲存在記憶體中。 這意味著每次執行 mypy 時,您不必載入與數千個導入依賴項相關的快取資料。 其次,他在小結構單元的層次上仔細分析了職能與其他實體之間的依賴關係。 例如,如果函數 foo 呼叫函數 bar,那麼就存在依賴性 foobar。 當文件更改時,守護程序首先單獨處理已更改的文件。 然後,它會查看該文件的外部可見更改,例如更改的函數簽名。 守護程序僅使用有關導入的詳細資訊來仔細檢查那些實際使用修改後的函數的函數。 通常,使用這種方法,您只需檢查很少的功能。

要實現所有這些並不容易,因為最初的 mypy 實作主要集中於一次處理一個檔案。 我們必須處理許多邊緣情況,這些情況的發生需要在程式碼變更的情況下重複檢查。 例如,當為類別指派新的基底類別時,就會發生這種情況。 一旦我們完成了我們想要的事情,我們就能夠將大多數增量檢查的執行時間減少到幾秒鐘。 這對我們來說似乎是一場巨大的勝利。

生產力更高!

與我上面討論的遠端快取一起,mypy 守護進程幾乎完全解決了程式設計師頻繁運行類型檢查、對少量檔案進行更改時出現的問題。 然而,在最不利的用例中,系統效能仍遠未達到最佳狀態。 mypy 的乾淨啟動可能需要 15 分鐘以上。 這遠遠超出了我們的預期。 隨著程式設計師繼續編寫新程式碼並向現有程式碼添加註釋,情況每週都會變得更糟。 我們的用戶仍然渴望更高的效能,但我們很高興能半途而廢。

我們決定回到早期關於 mypy 的想法之一。 即,將Python程式碼轉換為C程式碼。 使用 Cython(一個允許您將 Python 編寫的程式碼轉換為 C 程式碼的系統)進行實驗並沒有為我們帶來任何明顯的加速,因此我們決定重新考慮編寫自己的編譯器。 由於 mypy 程式碼庫(用 Python 編寫)已經包含所有必要的類型註釋,因此我們認為嘗試使用這些註釋來加速系統是值得的。 我很快就創建了一個原型來測試這個想法。 在各種微基準測試中,它的效能提高了 10 倍以上。 我們的想法是使用 Cython 將 Python 模組編譯為 C 模組,並將類型註釋轉換為運行時類型檢查(通常類型註釋在運行時被忽略,僅由類型檢查系統使用)。 實際上,我們計劃將 Python 的 mypy 實作轉換為設計為靜態類型的語言,看起來(並且在大多數情況下工作)與 Python 完全相同。 (這種跨語言遷移已經成為 mypy 專案的傳統。最初的 mypy 實作是用 Alore 編寫的,然後出現了 Java 和 Python 的語法混合)。

專注於 CPython 擴充 API 是不失去專案管理功能的關鍵。 我們不需要實作虛擬機器或 mypy 所需的任何函式庫。 此外,我們仍然可以存取整個Python生態系統和所有工具(例如pytest)。 這意味著我們可以在開發過程中繼續使用解釋的 Python 程式碼,使我們能夠繼續以非常快速的模式進行程式碼變更和測試,而不是等待程式碼編譯。 可以說,我們坐在兩張椅子上看起來做得很好,我們很喜歡它。

我們稱為 mypyc 的編譯器(因為它使用 mypy 作為分析類型的前端),結果是一個非常成功的專案。 總體而言,我們在沒有快取的情況下頻繁運行 mypy,實現了大約 4 倍的加速。 開發 mypyc 專案的核心花費了 Michael Sullivan、Ivan Levkivsky、Hugh Hahn 和我自己組成的小團隊大約 4 個日曆月的時間。 這個工作量比用 C++ 或 Go 重寫 mypy 所需的工作量要少得多。 而且我們對專案所做的更改比用另一種語言重寫時要少得多。 我們還希望能夠將 mypyc 提升到其他 Dropbox 程式設計師可以使用它來編譯和加速他們的程式碼的水平。

為了達到這種性能水平,我們必須應用一些有趣的工程解決方案。 因此,編譯器可以透過使用快速的低階 C 結構來加速許多操作,例如,將已編譯的函數呼叫轉換為 C 函數呼叫。 而且這樣的呼叫比呼叫解釋函數快得多。 一些操作(例如字典查找)仍然涉及使用來自 CPython 的常規 C-API 調用,這在編譯時僅稍微快一些。 我們能夠消除解釋對系統造成的額外負載,但在本例中,這僅在效能方面帶來了很小的效益。

為了識別最常見的「慢」操作,我們執行了程式碼分析。 有了這些數據,我們嘗試調整mypyc 以便它為此類操作生成更快的C 程式碼,或者使用更快的操作重寫相應的Python 程式碼(有時我們根本沒有足夠簡單的解決方案來解決該問題或其他問題) 。 重寫 Python 程式碼通常比讓編譯器自動執行相同的轉換更容易解決問題。 從長遠來看,我們希望實現其中許多轉換的自動化,但當時我們專注於以最小的努力加速 mypy。 在實現這一目標的過程中,我們走了一些捷徑。

待續...

親愛的讀者! 當您得知 mypy 專案的存在時,您對它的印像如何?

對 4 萬行 Python 代碼進行類型檢查的路徑。 第2部分
對 4 萬行 Python 代碼進行類型檢查的路徑。 第2部分

來源: www.habr.com

添加評論