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

今天,我們提請您注意有關 Dropbox 如何處理 Python 代碼類型控制的材料翻譯的第一部分。

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

Dropbox 用 Python 編寫了很多內容。 這是一種我們使用極其廣泛的語言,無論是後端服務還是桌面客戶端應用程序。 我們也經常使用 Go、TypeScript 和 Rust,但 Python 是我們的主要語言。 考慮到我們的規模,我們正在談論數百萬行Python代碼,事實證明,此類代碼的動態類型不必要地使其理解變得複雜,並開始嚴重影響勞動生產率。 為了緩解這個問題,我們已經開始逐漸將代碼轉換為使用 mypy 進行靜態類型檢查。 這可能是最流行的 Python 獨立類型檢查系統。 Mypy 是一個開源項目,其主要開發人員在 Dropbox 工作。

Dropbox 是首批在 Python 代碼中實現如此規模的靜態類型檢查的公司之一。 如今,Mypy 已在數千個項目中使用。 正如他們所說,這個工具無數次“在戰鬥中經過考驗”。 我們已經走了很長的路才到達現在的位置。 一路走來,也有很多不成功的嘗試和失敗的嘗試。 這篇文章介紹了 Python 中靜態類型檢查的歷史,從作為我的研究項目的一部分的艱難開始,到現在類型檢查和類型提示對於無數使用 Python 編寫的開發人員來說已經變得司空見慣。 這些機制現在得到許多工具的支持,例如 IDE 和代碼分析器。

閱讀第二部分

為什麼需要類型檢查?

如果您曾經使用過動態類型的 Python,您可能會對為什麼最近靜態類型和 mypy 如此大驚小怪感到困惑。 或者也許您喜歡 Python 正是因為它的動態類型,而正在發生的事情只是讓您感到不安。 靜態類型價值的關鍵在於解決方案的規模:您的項目越大,您就越傾向於靜態類型,最終,您就越需要它。

假設某個項目已經達到了幾萬行的規模,結果發現有幾個程序員正在開發它。 看看類似的項目,根據我們的經驗,我們可以說理解其代碼將是保持開發人員生產力的關鍵。 如果沒有類型註釋,則可能很難弄清楚要傳遞給函數的參數是什麼,或者函數可以返回什麼類型。 以下是不使用類型註釋通常很難回答的典型問題:

  • 這個函數可以返回嗎 None?
  • 這個論點應該是什麼? items?
  • 什麼是屬性類型 id: int 是嗎, str,或者也許是一些自定義類型?
  • 這個參數應該是一個列表嗎? 是否可以將元組傳遞給它?

如果您查看以下帶類型註釋的代碼片段並嘗試回答類似的問題,就會發現這是最簡單的任務:

class Resource:
    id: bytes
    ...
    def read_metadata(self, 
                      items: Sequence[str]) -> Dict[str, MetadataItem]:
        ...

  • read_metadata 不返回 None,因為返回類型不是 Optional[…].
  • 爭論 items 是一系列行。 不能隨機迭代。
  • 屬性 id 是一串字節。

在理想的世界中,人們會期望所有這些微妙之處都將在內置文檔(文檔字符串)中進行描述。 但經驗提供了很多例子,表明這樣的文檔通常不會在您必須使用的代碼中被觀察到。 即使代碼中存在此類文檔,也不能指望其絕對正確。 該文檔可能含糊、不准確並且容易產生誤解。 在大型團隊或大型項目中,這個問題可能變得極其嚴重。

雖然 Python 在項目的早期或中期階段表現出色,但在某些時候,使用 Python 的成功項目和公司可能會面臨一個至關重要的問題:“我們應該用靜態類型語言重寫所有內容嗎?”。

像 mypy 這樣的類型檢查系統通過為開發人員提供描述類型的形式化語言並檢查類型聲明是否與程序實現相匹配(以及可選地檢查它們的存在)來解決上述問題。 一般來說,我們可以說這些系統為我們提供了諸如仔細檢查的文檔之類的東西。

使用此類系統還有其他優點,而且它們已經非常重要:

  • 類型檢查系統可以檢測到一些小的(而且不是那麼小的)錯誤。 一個典型的例子是當他們忘記處理一個值時 None 或其他一些特殊情況。
  • 代碼重構大大簡化,因為類型檢查系統通常非常準確地了解需要更改的代碼。 同時,我們不需要希望通過測試獲得 100% 的代碼覆蓋率,無論如何,這通常是不可行的。 我們不需要深入研究堆棧跟踪的深處來找出問題的原因。
  • 即使在大型項目中,mypy 通常也可以在不到一秒的時間內完成完整的類型檢查。 而測試的執行通常需要幾十秒甚至幾分鐘的時間。 類型檢查系統為程序員提供即時反饋,使他能夠更快地完成工作。 他不再需要編寫脆弱且難以維護的單元測試,用模擬和補丁替換真實實體,只是為了更快地獲得代碼測試結果。

PyCharm 或 Visual Studio Code 等 IDE 和編輯器利用類型註釋的強大功能為開發人員提供代碼補全、錯誤突出顯示以及對常用語言結構的支持。 這些只是打字的一些好處。 對於一些程序員來說,所有這些都是支持打字的主要論點。 這是實施後立即受益的事情。 此類型用例不需要單獨的類型檢查系統(例如 mypy),但應該注意的是,mypy 有助於保持類型註釋與代碼一致。

mypy背景

mypy 的歷史始於英國劍橋,幾年前我加入 Dropbox。 作為我博士研究的一部分,我參與了靜態類型和動態語言的統一。 我受到了 Jeremy Siek 和 Walid Taha 撰寫的關於增量打字的文章以及 Typed Racket 項目的啟發。 我試圖找到在各種項目中使用相同編程語言的方法 - 從小型腳本到由數百萬行組成的代碼庫。 與此同時,我想確保在任何規模的項目中,都不必做出太大的妥協。 所有這一切的一個重要部分是逐漸從無類型原型項目轉變為經過全面測試的靜態類型成品的想法。 如今,這些想法在很大程度上被認為是理所當然的,但在 2010 年,這是一個仍在積極探索的問題。

我最初在類型檢查方面的工作並不是針對 Python 的。 相反,我使用了一種小型的“自製”語言 阿洛爾. 這是一個示例,可以讓您了解我們正在討論的內容(此處類型註釋是可選的):

def Fib(n as Int) as Int
  if n <= 1
    return n
  else
    return Fib(n - 1) + Fib(n - 2)
  end
end

使用簡化的母語是科學研究中常用的方法。 之所以如此,不僅是因為它可以讓你快速進行實驗,還因為與研究無關的東西很容易被忽略。 現實世界的編程語言往往是具有復雜實現的大規模現象,這會減慢實驗速度。 然而,任何基於簡化語言的結果看起來都有點可疑,因為在獲得這些結果時,研究人員可能犧牲了對語言實際使用重要的考慮。

我的 Alore 類型檢查器看起來非常有前途,但我想通過試驗真實代碼來測試它,你可能會說,這些代碼不是用 Alore 編寫的。 對我來說幸運的是,Alore 語言很大程度上基於與 Python 相同的思想。 更改類型檢查器非常容易,以便它可以與 Python 的語法和語義配合使用。 這使我們能夠嘗試在開源 Python 代碼中進行類型檢查。 此外,我還編寫了一個轉譯器,將用 Alore 編寫的代碼轉換為 Python 代碼,並用它來翻譯我的類型檢查器代碼。 現在我有了一個用 Python 編寫的類型檢查系統,它支持 Python 的一個子集,即某種語言! (某些對 Alore 有意義的架構決策不太適合 Python,這一點在 mypy 代碼庫的某些部分中仍然很明顯。)

事實上,我的類型系統支持的語言此時還不能完全稱為 Python:由於 Python 3 類型註釋語法的一些限制,它是 Python 的一個變體。

它看起來像是 Java 和 Python 的混合體:

int fib(int n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

我當時的想法之一是使用類型註釋來通過將這種 Python 編譯為 C 或 JVM 字節碼來提高性能。 我已經到了編寫編譯器原型的階段,但我放棄了這個想法,因為類型檢查本身看起來非常有用。

我最終在聖克拉拉舉行的 PyCon 2013 上展示了我的項目。 我還與吉多·範羅蘇姆(Guido van Rossum)討論過這個問題,他是仁慈的 Python 終身獨裁者。 他說服我放棄自己的語法並堅持使用標準的 Python 3 語法。Python 3 支持函數註釋,因此我的示例可以如下所示重寫,從而得到一個正常的 Python 程序:

def fib(n: int) -> int:
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

我必須做出一些妥協(首先,我想指出,正是出於這個原因,我發明了自己的語法)。 特別是,當時該語言的最新版本 Python 3.3 不支持變量註釋。 我通過電子郵件與 Guido 討論了此類註釋的語法設計的各種可能性。 我們決定對變量使用類型註釋。 這達到了預期的目的,但有點麻煩(Python 3.6 給了我們更好的語法):

products = []  # type: List[str]  # Eww

類型註釋在支持 Python 2 方面也派上用場,Python XNUMX 沒有對類型註釋的內置支持:

f fib(n):
    # type: (int) -> int
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

事實證明,這些(和其他)權衡並不重要 - 靜態類型的好處意味著用戶很快就會忘記不太完美的語法。 由於經過類型檢查的Python代碼中沒有使用特殊的語法結構,現有的Python工具和代碼處理流程可以繼續正常工作,使開發人員更容易學習新工具。

在我完成研究生論文後,Guido 還說服我加入 Dropbox。 這是 mypy 故事中最有趣的部分開始的地方。

待續...

親愛的讀者! 如果您使用 Python,請告訴我們您用這種語言開發的項目的規模。

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

來源: www.habr.com

添加評論