Python 程式碼品質與設計
使用時機
- 撰寫或審查能運行但難以理解、擴展或維護的 Python 程式碼。
- 將直接從 Java 或 C++ 翻譯而來的結構,替換為更適合該語言的建構。
- 選擇抽象化、評估耦合度,或在新需求出現後進行重構。
- 辨識模式的意圖,而不將其名稱視為架構上的義務。
- 此範圍涉及對程式碼的推理;它既不指定部署環境,也不指定框架。
哲學
尋求能解決觀察到問題的最小建構,而非想像中最通用的設計。 當 Python 已提供某模式的結構時,保留該結構的意圖;移除該結構是一種改進,而非缺乏精緻度。 好的抽象化能在不抹除客戶端所需資訊的前提下,降低變更成本。 源自 Bruce Eckel 所著《Thinking in Python》的實務綜合;語料庫 a8feb860。 尊重專案實際支援的 Python 版本:語料庫的近期目標並不授權任何隱含的版本升級。
規則
可讀的語法與可預測的名稱
- 讓縮排與實際的區塊結構相符;使用四個空格,不要混用 Tab 和空格。
- 區塊標頭結尾使用冒號;避免使用分號和多個陳述式來人為壓縮程式碼。
- 對於較長的陳述式,使用括號、方括號或大括號內的續行,而非難以跟隨的分行方式。
- 不要混淆區塊與作用域:在
if或for中的指派,在區塊結束後仍可能保持可見。 - 檢查在名稱被使用之前,每個執行路徑上是否都有指派;僅靠縮排無法保證其綁定。
- 變數、函式、方法和檔案使用
snake_case;類別使用PascalCase;常數使用大寫。 - 保留以函式樣式命名的類別這個例外,僅用於實際以該方式使用的物件;不要將其視為通用風格。
- 不要從大寫名稱推斷執行時期的保護:它對讀者傳達的是意圖。
- 保留專案一致的慣例,並使用其現有的 linter,而非建立任意的本地規則。
參考、值與可變性
- 將指派理解為將名稱綁定到物件;它不會複製該物件。
- 明確區分重新指派(reassign)與變異(mutate):重新指派名稱不會改變其先前的別名,而變異物件則可能影響所有別名。
- 使用
==進行值相等比較;將is保留用於預期的身分比較,特別是用於is None和其他單例(singleton)。 - 不要使用字串或小整數的身分作為值測試:共用最佳化並不提供此合約。
- 清單的切片或淺層複製會重建外部結構,而非巢狀元素;檢查哪些物件仍然被共用。
- 僅當也需要複製包含的物件時,才使用深層複製;指派無法提供這種隔離。
- 不要機械性地將
x += y替換為x = x + y:對於清單,第一種形式會原地修改它,而第二種則會產生一個新的。 - 對於不可變的數字,理解擴增指派是將名稱重新綁定到結果,而非修改共用的數字。
- 當資料不需要演化時,偏好使用不可變資料,但不要將容器的不可變性與其內容的不可變性混淆。
數字與真值
- 區分
/(對於整數運算元會產生浮點數的除法)和//(向負無窮大進位的除法)。 - 在移植時測試負數情況:整數商並非向零截斷,且餘數
%的符號會跟隨除數。 - 不要假設 Python 整數有固定寬度;它們不像其他語言的有限整數那樣會溢位。
- 將精確的整數算術與浮點數算術分開;不要將一個的保證引入另一個。
- 考慮
round()和格式化時的「四捨六入五成雙」規則;不要假設四捨五入總是遠離零。 - 在字面量中使用
_分隔符,讓大數字易於閱讀,且不改變其值。 - 使用
+= 1和-= 1來遞增或遞減;Python 沒有++或--運算子。 - 布林值也是整數:對條件求和可以計數真值情況,但
True並不構成一個與int不同數值型別。 - 當問題僅僅是容器是否為空時,偏好使用
if items:而非長度測試。 - 記住物件的真值可以由
__bool__()定義,或者若無則由__len__()定義。 and和or會短路並返回一個運算元,不一定是布林值;不要僅憑其名稱來註釋它們的結果。- 僅在應該替換所有假值時才使用
x or default;明確使用None測試來保留零或空文字。 - 對於遮罩,區分位元運算子與邏輯運算子;
~x不會反轉一個固定寬度的字組。 - 不要期望
@對內建數字執行乘積:矩陣乘法取決於型別,且與裝飾器語法不同。
字串與插值
- 字串是不可變的:如果需要重複使用轉換結果,請保留它。
- 選擇能限制跳脫的引號;當換行是文字的一部分時,使用三重引號。
- 對於文字反斜線,使用原始字串(raw string),並注意無法以單一反斜線結束該字面量的限制。
- 使用內建的搜尋、分割、替換和聯結操作,而非在沒有特定需求的情況下使用字元操作迴圈。
- 不要使用
strip、lstrip或rstrip來移除精確的前綴:它們的引數表示字元,而非不可分割的子字串。 - 當目標版本支援時,使用
removeprefix()和removesuffix()來移除精確的開頭或結尾子字串。 +不會隱式地將數字轉換為字串;使用明確的轉換或 f-string。- 偏好使用 f-string 來產生一般文字;其表達式會被求值,且結果會立即呈現。
- 使用格式規範來指定精度、寬度和對齊方式;
!r顯示適合診斷的表示形式,=則為表達式加上標籤。 - 不要僅僅因為舊的
%或str.format()用法存在就改寫它們:它們仍然有效,且遷移必須能改善相關程式碼。 - 僅當目標版本支援且消費者需要在組裝之前區分字面量與值時,才考慮使用 t-string。
- t-string 產生的是結構化物件,而非完成的字串;消費者必須根據其領域實際地引用、跳脫或拒絕插值。
- 不要僅基於插值語法就承諾安全性,也不要將說明性的字元拒絕機制重新用作通用保護。
- 模板消費者不得假設文字和值之間嚴格交替:迭代可能會省略空字面量。
抽象化、相依性與內聚力
- 在新增類別之前,檢查函式、字典、模組或語言協定是否足夠。
- 僅僅因為一個類別託管了一個方法,並不能僅憑此組織方式就證明其正當性。
- 基於實際需求來命名變化的軸向;反覆在同一位置發生的變更,比預期的靈活性更有用的證據。
- 將會變化的部分與保持穩定的部分分開;如果沒有東西在變化,就不要建立變化機制。
- 將慣用語、特定解決方案和通用模式視為可能的階段,而非必須攀登的階梯。
- 對於每個抽象化,識別它所抹去的細節,並確認沒有任何正當的客戶端需要恢復它們。
- 同時檢查介面背後隱藏的工作:複製、排序或間接存取在實際資料規模下可能變得主導。
- 透過一個部分知道什麼來評估耦合度:父類別細節、具體類別、介面、呼叫簽章或簡單的值。
- 不要將型別化的相依性與沒有相依性混淆:
Protocol避免了宣告式的繼承,但其成員仍然是合約。 - 當客戶端只需要一個操作時,偏好使用函式簽章;不要在沒有用途的情況下建立多方法介面。
- 模式通常會移動相依性而非移除它;找出仍然必須命名具體類別的部分。
- 當具體相依性被刻意放置在更換成本低的組合點時,接受它。
- 不要深入間接取得的物件的內部細節;透過明確的介面請求所需的服務。
- 將服務於同一主題的元素分組到一個元件中;將可以獨立演化的想法分開。
- 偏好使用組合來變化協作者,而無需繼承其實作細節;如果可替代性是真實的,則保留繼承。
- 檢查子型別是否滿足基底型別客戶端的期望,而不僅僅是方法的名稱。
- 提取表達相同意圖且出於相同原因的重複程式碼;偶然的相似性是不夠的。
- 讓常見情況簡單、罕見情況可行、介面一致;行為上的例外會造成記憶成本。
- 如果移除一個層級既不會移除必要的行為,也不會移除有用的隔離,就移除它;在兩個足夠的解決方案中,選擇較簡單的那個。
- 在可能的情況下,讓計算保持純粹:相同輸入產生相同結果,且沒有任何可觀察到的外部變化。
沿著擴展的兩個軸向重構
- 區分新增型別與新增操作;設計通常會偏重其中一個而犧牲另一個。
- 列舉開放家族所有子類別的
match需要維護清單,且可能會默默地忽略新型別。 - 如果操作僅按確切類別分組,則使用以
type(obj)為索引的字典可以避免此列舉。 - 不要將此替換視為純語法性的:子類別隨後會獲得自己的鍵,這與父類別模式不同。
- 對於首次出現時建立的群組,
defaultdict(list)提供了新的清單;字典註解不會建立此行為。 - 將統一的操作保留為讀取相關屬性的普通函式;如果演算法不因型別而異,則分發機制不會增加任何價值。
- 當每個型別自然地攜帶其回應且操作很少時,將方法保留在小型、受控的層級結構上。
- 考慮使用
functools.singledispatch來新增因型別而異的操作,而無需修改外部層級結構或將新操作散佈到每個類別中。 - 使用
singledispatch時,實作是根據第一個引數選擇的,並且可能根據 MRO 被繼承;它不是一個確切類別的表格。 - 預設回應必須對未註冊的型別真正有效;否則引發
NotImplementedError以使遺漏變得明顯。 - 不要從任意模組新增方法:這會迫使讀者在類別定義之外搜尋其行為。
- 根據變更的位置和保留的資料來衡量重構,而不僅僅是移除的行數。
使用模式詞彙而不強加目錄
- 先描述問題:建立、介面適配、演算法選擇、通知、樹狀結構或狀態恢復;名稱之後再決定。
- 當傳遞或儲存函式已足夠時,簡化 Strategy 和 Command;鏈可以是一系列按順序嘗試的函式清單。
- 認識到共用模組、類別表格或迭代協定可能已經滿足 Singleton、Factory 或 Iterator 的意圖。
- 區分 Adapter(轉換介面)、Façade(簡化子系統)和 Proxy(控制對另一個物件的存取)。
- 區分 State(根據狀態改變行為)與狀態機(根據輸入選擇轉換)。
- 區分從外部注入協作者與在註冊表中查找該協作者;兩者都以不同方式轉移控制權,而不會暴露相依性。
- 將架構、持久化、訊息和雲端名稱保持在所描述意圖的層級:它們在目錄中的存在並不建立實作配方。
- 不要從僅僅提及 Circuit Breaker、Saga、Repository 或 Event Sourcing 就推斷出部署、恢復或一致性的建議。
步驟
- 識別目標 Python 版本、慣例和已使用的檢查;記錄要保留的行為。
- 陳述具體需求與觀察到的變化軸向;列出目前受影響的元件。
- 檢查涉及的共用參考、真值、數值運算和字串轉換。
- 撰寫最小解決方案,然後比較提議的抽象化:抹去的細節、隱藏的工作和移動的相依性。
- 分別測試新增型別和新增操作;注意每個變體中所需的變更。
- 分階段重構,同時保留行為測試以及傳入和傳出資料的計數。
- 移除已變得不必要的層級,然後在專案支援的版本上執行其測試和檢查。
陷阱
- 程式可能在遺失新型別後產生看似合理的總數:請檢查資料保留,而不僅僅是輸出呈現。
- 備援分支或預設實作可能會隱藏被遺忘的擴展;如果沒有中性的回應是正確的,則偏好明確的失敗。
- 在擁有幾個實際案例之前進行一般化,會創造沒有證據證明其有用性的選項;拒絕推測性的框架。
- 簡短的抽象化可能隱藏高昂的成本或抹去不可或缺的細節;僅計數行數不足以判斷其品質。
- 語料庫中的最新語法和 API 不一定適用於較舊的專案;不要為了採用它們而提高專案的最低 Python 版本。
驗證
- 每個新的類別或介面都保護一個已命名的需求;沒有模式只是為了其名稱而存在。
- 條件路徑在使用變數前已綁定它們;身分、變異和相等性沒有被混淆。
- 當零、空值、
None、負數和巢狀資料的區別至關重要時,它們已被測試過。 - 新的子類別不會默默消失,並且確切類別或繼承行爲已得到充分考慮。
- 移除一個層級不會暴露一個先前隱藏的耦合,也不會破壞必要的隔離。
- 重構保留了傳入和傳出的資料,以及它們具有任何意義的順序。
- 文件已更新以反映任何新的抽象化或簡化。