強健的 Python強健的 Python

這是《強健的 Python|撰寫潔淨且可維護的程式碼》筆記的第 1 篇,你可以把它當作是一則重點整理,加上我個人的開發經驗與心得。

本書翻譯自《Robust Python: Write Clean and Maintainable Code》,原文的副標題容易讓人以為這又是另一本關於「Clean Code in Python」的書。

實際上,本書所聚焦的,是 Python 的一大特性——type hints。全書大半篇幅都圍繞著這個核心主軸。

所以它並非教你「如何寫出 Pythonic 程式碼」的書,而是介紹 type hints 寫法與使用相關工具(比如 Mypy)以確保 type hints 有效落實的作品。

如果你還不熟悉 Python type hints,本文也可以作為認識 type hints 的起點。


場景與緣由

上個月,我在一個新的 Django 工作專案中加入了 Mypy,主要做了這兩件事:

  1. 整合 Mypy VS Code 套件(包括 Mypy 設定檔),讓開發者在寫程式的當下,可以在編輯器中隨時收到來自 Mypy 的「提醒」。
  2. 設定 Mypy pre-commit hook。

相關文章:Python 開發:pre-commit 設定 Git Hooks 教學

上述二者(主要是第二點),讓新專案真正落實了 Python type hints——這下子你不寫 type hints 也不行了。

不過還差 CI——預計今年上半年補完。

這個過程並不困難,但還是需要一番摸索才能順利到位,這部分我會另外寫成一篇文章作為教學。

本文的重點,在於專案落實 type hints 的挑戰

專案落實 type hints 的真正難點

在為 Django 專案導入 Mypy 之前,我讀了本書——前後讀了兩次。

之所以寫這篇筆記,主要是深刻感受到,在工作上要落實 type hints、把它們加入到 Python 專案中,整個環節最難的部分,絕不是關於 type hints 語法的學習。

而是其它種種因素。

事實上,我去年就打算為 Django 專案全面導入 type hints,但是失敗了!直到本次的時機「更加成熟」,才下定決心推行。

本文藉由對書中內容的整理與我的經驗,好好介紹一下,為專案加入 type hints 時,你一定會遇到的困難,與對應的思考。


Python type hints 簡介

Python 3.5 首次導入了 type hints,詳情請見〈PEP 484 – Type Hints〉,該 PEP 提案的共同發起人除了 Python 之父 Guido van Rossum,還有 Mypy 的作者。

接下來的每一版 Python,都少不了對 type hints 的擴充與增強。可以說,type hints 是經過「一系列」漫長的發展與迭代,才有了今日的氣象。

Type hints 的野心

Guido van Rossum 本人對 type hints 非常重視,從良葛格的〈Type Hints 的野心〉中這一段話可知:

本來 Python 在動態定型語言中,相對來說,就是極為重視工程性的語言,然而曾經在某個地方看過的說法是,Guido 長年以來的野心,就是讓 Python 在工程性上更進一步,而在靜態分析工具上投入精力會是必要的。事實上,從 2000 年以來,Guido 一直想在 Python 中,加入可選的靜態定型,一直到 PEP 484、526 的實現,本人也在 PyCon 2015 親自上場講演〈Type Hints〉,似乎也印證了這點。

Type hints 的價值

專案程式碼加入 type hints 的好處顯而易見,它們賦予 Python 在開發階段就能如靜態型別語言般,為變數、參數、回傳值明示型別的能力,從而提高專案的穩定性和可維護性——這在大型專案中尤其重要。

說真的,很多時候我光是看型別,對於程式碼的理解就有明顯的提升,這對於程式碼的閱讀、維護、重構都有很大的幫助

搭配 Mypy 這類 type checker,加上 IDE 整合,更是妙不可言!

這直接提高了 Python 在團隊協作的競爭力,讓大型專案採用 Python 變得更加可能——當然,我知道,有些人並不這麼認為😎

Type hints 的代價

儘管 type hints 有種種好,但平心而論,它的代價也不小!(後面會提到)

所以有時候,你決定不採用 type hints,或許是「明智」的,尤其是在一些不需要長期維護的專案。

但是,要徹底了解 type hints 的優點與缺點,我們需要更加全面且務實的的觀點。


呼,鋪陳有點長,以下才是本書的重點整理!

Type hints 的利弊分析

關於採用 type hints 的利弊分析,本書的第 100 頁,有相對完整的論述。

利的部分上面已經說了,這裡我們主要關注導入 type hints 的「缺點」。

在此直接引用書中所舉的內容(我加上粗體作為重點標示),講述專案導入 type hints 的阻力與成本,包括:

  • 需要獲得支持。根據文化的不同,可能需要一些時間來說服一個組纖採用型別檢查。
  • 一旦你獲得支持,就會有一個最初的採用成本。開發人員不會在一夜之間就能開始對他們的程式碼進行型別注釋,他們需要時間來掌握。他們需要學習和實驗,然後才能實際動手。
  • 採用工具需要時間和努力。你需要某種形式的中央化檢查,而開發者需要熟悉工具作為他們工作流程一部分的執行。
  • 在你的專案中編寫型別注釋需要時間。
  • 隨著型別注釋開始受到檢查,開發人員將不得不習慣於與型別檢查器對抗所帶來的速度減慢。思考型別帶來了額外的認知負載。

不愧是專門討論 Python type hints 的書,我只能說,每一條都非常真實!這也是為何我們在今年才成功導入,去年可以說還沒有準備好——但也不是毫無收獲。

如作者所言:

這個問題從根本上說,就是一種雞和蛋的難題:你在專案中寫下足夠多的型別注釋之前,你不會看到注釋型別的好處。然而,在早期沒有效益的情況下,要讓人接受去撰寫型別是很困難的

綜上所述,為專案加上 type hints 看起來困難重重,難道我們要就此放棄嗎?

當然不。否則作者不需要寫這本書,而我也不需要寫這篇文章。

與之相反,我們不妨更早開始學習 type hints ,早點達到並跨過書中所謂的「投入與收益的平衡點」。


更早「收支平衡」

為了使型別注釋的效益最大化,你需要更早取得價值或更早降低成本。這兩條曲線的交點是一個收支平衡點(break-even point),這就是你所付出的努力因為你所得到的價值而有報償的地方。

Type hints 的投入與報酬關係,書中用了一張圖來說明,我重繪如下:

作者解釋道:

你的成本一開始會很高,但隨著採用率的提高會變得更和緩。你的效益一開始會很低,但隨著你注釋 codebase,你會看到更多的價值。在這兩條曲線相遇之前,你不會看到投資的回報。為了使價值最大化,你需要儘早達到這個交叉點。

講白了就是,寫愈多,阻力就會愈低。這雖然像是「廢話」,但也給了我們希望!🤣

對於這張圖,我的看法是:type hints 的收益絕對是累積出來的,但成本的下降不一定有這麼快

畢竟每次寫 type hints,多少都需要思考(所幸我們有 GitHub Copilot 🥰),這個成本未必會隨著時間而快速降低——但肯定會逐漸降低,因為習慣了。


文末我會提出自己對於「降低導入 type hints 阻力」的看法。我們先來看書中的建議。

書中的一些建議

你想在能維持前進動力的情況下,儘快達到這個點,以讓你的型別注釋帶來正面的影響。這裡有一些策略可以做到這一點。

作者給出的大方向,就是先為「部分」——而非全部——程式碼加上 type hints。讓你可以早點感受到 type hints 帶來的好處。

那要「怎麼選擇」哪些程式碼應該優先加上 type hints?他的看法大概有下:

  • 只為新程式碼進行型別注釋(這裡的新程式碼包括了對舊程式碼的修改
  • 型別注釋「為你賺錢」的程式碼
  • 型別注釋「經常變化」的程式碼
  • 型別注釋「複雜」的程式碼

以上這幾點取自書中的小標題,但我想說的是——這些看法雖然有道理,但其實大部分都很難操作!

什麼叫賺錢的程式碼?怎麼樣才算複雜?書中並非沒有解釋,但這些都需要人為定義,而且有「標準浮動」之嫌,根本難以一體適用。

為新程式碼加上 type hints

「為新程式碼加上 type hints 」部分,是我唯一認為值得參考且可行的!

考慮讓你當前未注釋的程式碼保持原樣,並根據這兩條規則來注釋程式碼:

  • 為你所寫的任何新程式碼進行注釋。
  • 注釋你變更的任何舊程式碼

對於已經完成的舊專案,我們就是採用這個策略!畢竟要把舊專案整個翻新成有 type hints 的版本,成本往往很高。

所以採折衷方案,比如修改舊函式的其中幾行、或寫一個全新的函式時,該函式就必須有完整的 type hints——我會在 code review 時進行檢查。

這樣的好處是標準清晰、判斷簡單,而且不用一次到位。

當然,想要透過慢慢更新,最終讓整個專案都可以通過 Mypy 檢查,還是不太現實的。這算是一種「對舊的、暫時無力全面適用 type hints 專案的折衷」。

並非所有程式碼都要加上 type hints

有件事必須釐清與強調,以免造成誤解。

所謂「完整」的 type hints,並不是把專案程式碼中的每一個地方都加上 type hints。

變數加上 type hints 往往過於囉嗦而且實益不大(書中 39 頁也有提及),我們只會偶爾為之——因為大部分情況直譯器、IDE、type checker 都能夠自行推測了!

那到底怎麼樣才算「完整」?

一般而言,為所有的函式、類別中的方法加上 type hints,就已經很不錯。

不過,Mypy 畢竟提供了一個標準。讓你不必自己去判斷。只要使用 Mypy 的基本設定檢查整個專案而且不報錯,我認為就算完整了。

當然你可以排除檢查一些檔案,比如 Django 中的「資料庫遷移檔」。


我的經驗與看法

去年想為 DRF 專案加入完整的 type hints(也就是要可以通過 Mypy 檢查的程度),最後放棄了。

主要有兩個原因:

  1. DRF 本身並不要求型別,而且專案誕生的早,也沒有完整的型別支援。不過現在有 stubs 套件(專門標示 type hints 的套件,比如 djangorestframework-stubs),這應該不至於成為關鍵難點。
  2. 這個才是我覺得最難的,那就是「不易說服」成員寫 type hints。儘管 type hints 肯定有好處,但我沒有信心,這好處會明顯超過投入的心力——尤其在一開始的時候。

顯然我不太可能只是給同事看上面那張圖,就能輕易說服大家要開始寫 type hints 🐸

所以最後我還是放棄「完整」type hints 的企圖,只是要求大家要開始「試著」寫 type hints。(事實證明,這樣也不錯,至少大家開始有了 type hints 的概念)

Django Ninja 與 Pydantic

那怎樣才能讓我在「事前」就有足夠信心說服大家呢?

就是採用像 FastAPI、Django Ninja 這類,透過 Pydantic 來產生 API 文件的框架!

API 文件是後端開發的一大痛點,DRF 雖然也有 drf-yasg 這樣的套件,但說真的,和 Pydantic 這種原生採用 type hints 來產生 API 文件相比,肯定有所不及。

今年的新專案開始用了 Django Ninja,因此情況大不相同——即時機成熟了。

Type hints 輔助輪

寫 Django Ninja(或 FastAPI),你本來就要書寫大量 type hints,來產生正確、合理的 API 文件。那要求為專案的其餘部分,比如自定義的函式、類別加上 type hints,阻力相對就小得多。

這主要差別在於,大家能因為 API 文件的自動渲染,直接感受到 type hints 的價值!

其實不止如此,剛學習 Django Ninja 時,我還發出過這樣的讚嘆

竟然可以充分運用 Python type hints 到這般程度,讓它不僅僅是為了型別安全而服務,而是融入到整個 api 流程中

在我看來,這類採用了 Pydantic 框架所帶來的效應,就像腳踏車的輔助輪,在剛起步使用 type hints 時,有著明顯的正面引導效果。


小結與整理

然而,我並不是說,只有寫 FastAPI 或 Django Ninja 才能成功落實 type hints,只不過它們遇到的阻力會相對小

如果能夠先有這類專案作為「甜頭」,那要在其餘專案落實可能會容易得多。因為要讓成員「相信」寫 type hints 有價值,確實不容易。

經過一段時間,目前我與我的同事們,都對 type hints 抱持著正面態度,因為很多錯誤可以在非常早的階段(也就是開發的當下)就被發現。(比如 Django ORM 物件的屬性根本寫錯了!)

省去了後續來回修正的時間浪費。

個人看法

最後,我再整理一下「降低導入 type hints 阻力」的一些個人看法:

  1. 建議從全新的專案開始,因為要將舊專案改為「type hints 版本」會困難得多。
  2. 建議從 FastAPI、Django Ninja,或其它採用了 Pydantic 的專案開始,成員會更有動力
  3. 雖然 type hints 對大專案有更顯著的效益,但建議從小型專案開始著手,以降低成員們練習、適應與接納的阻力。

綜上所述,你可以看出,要落實 type hints,其實沒有多少「捷徑」可走。

這也是為何我們需要了解其中的困難,才更可能適度地堅持,直到獲得 type hints 帶來的長期優勢