在我之前的文章中,我強調了在 Python 開發中有效專案管理的重要性。現在,讓我們將焦點轉向程式碼本身,探索如何編寫乾淨且易於維護的程式碼——這是在專業和合作環境中必不可少的做法。
可讀性與可維護性:結構良好的程式碼更容易閱讀、理解和修改。其他開發者——甚至未來的你自己——可以快速掌握邏輯,而不必掙扎於混亂的程式碼。
除錯與故障排除:有組織的程式碼,搭配清晰的變數名稱和結構化的函數,使得識別和修復錯誤變得更加高效。
可擴展性與重用性:模組化且組織良好的程式碼可以在不同專案中重用,讓擴展變得無縫,而不會干擾現有功能。
因此,當你在進行下一個 Python 專案時,請記住:
好程式碼的一半是乾淨的程式碼。
簡介
Python 是最受歡迎且多功能的程式語言之一,因其簡單性、易懂性和龐大的社群而受到讚賞。無論是網頁開發、數據分析、人工智慧還是任務自動化——Python 提供了強大且靈活的工具,適用於廣泛的領域。
然而,Python 專案的效率和可維護性在很大程度上取決於開發者所使用的做法。程式碼結構不良、缺乏慣例或甚至缺乏文檔,都可能迅速將一個有潛力的專案變成一個需要大量維護和開發的難題。正是這一點,使得學生的程式碼和專業的程式碼之間存在差異。
本文旨在介紹編寫高品質 Python 程式碼的最重要最佳實踐。遵循這些建議,開發者可以創建不僅功能正常,還可讀性高、性能良好且易於第三方維護的腳本和應用程式。
從專案開始就採用這些最佳實踐,不僅能確保團隊內部的更好協作,還能讓你的程式碼隨著未來需求的變化而演變。無論你是初學者還是經驗豐富的開發者,這份指南旨在支持你在所有 Python 開發中的工作。
程式碼結構
在 Python 中,良好的程式碼結構是必不可少的。主要有兩種專案佈局:平面佈局和 src 佈局。
平面佈局將源程式碼直接放在專案根目錄中,而不需要額外的資料夾。這種方法簡化了結構,適合小型腳本、快速原型和不需要複雜打包的專案。然而,這可能在運行測試或腳本時導致意外的導入問題。
📂 my_project/
├── 📂 my_project/ # 直接在根目錄
│ ├── 🐍 __init__.py
│ ├── 🐍 main.py # 主要進入點(如果需要)
│ ├── 🐍 module1.py # 範例模組
│ └── 🐍 utils.py
├── 📂 tests/ # 單元測試
│ ├── 🐍 test_module1.py
│ ├── 🐍 test_utils.py
│ └── ...
├── 📄 .gitignore # Git 忽略的檔案
├── 📄 pyproject.toml # 專案配置(Poetry, setuptools)
├── 📄 uv.lock # UV 檔案
├── 📄 README.md # 主要專案文檔
├── 📄 LICENSE # 專案許可證
├── 📄 Makefile # 自動化常見任務
├── 📄 DockerFile # 自動化常見任務
├── 📂 .github/ # GitHub Actions 工作流程(CI/CD)
│ ├── 📂 actions/
│ └── 📂 workflows/
另一方面,src 佈局(src 是 source 的縮寫)將源程式碼組織在專用的 src/ 目錄中,防止從工作目錄意外導入,並確保源檔案與其他專案組件(如測試或配置檔案)之間的清晰分隔。這種佈局非常適合大型專案、庫和生產就緒的應用程式,因為它強制正確的包安裝並避免導入衝突。
📂 my-project/
├── 📂 src/ # 主要源程式碼
│ ├── 📂 my_project/ # 主要包
│ │ ├── 🐍 __init__.py # 使資料夾成為包
│ │ ├── 🐍 main.py # 主要進入點(如果需要)
│ │ ├── 🐍 module1.py # 範例模組
│ │ └── ...
│ │ ├── 📂 utils/ # 工具函數
│ │ │ ├── 🐍 __init__.py
│ │ │ ├── 🐍 data_utils.py # 數據函數
│ │ │ ├── 🐍 io_utils.py # 輸入/輸出函數
│ │ │ └── ...
├── 📂 tests/ # 單元測試
│ ├── 🐍 test_module1.py
│ ├── 🐍 test_module2.py
│ ├── 🐍 conftest.py # Pytest 配置
│ └── ...
├── 📂 docs/ # 文檔
│ ├── 📄 index.md
│ ├── 📄 architecture.md
│ ├── 📄 installation.md
│ └── ...
├── 📂 notebooks/ # Jupyter Notebooks 用於探索
│ ├── 📄 exploration.ipynb
│ └── ...
├── 📂 scripts/ # 獨立腳本(ETL、數據處理)
│ ├── 🐍 run_pipeline.py
│ ├── 🐍 clean_data.py
│ └── ...
├── 📂 data/ # 原始或處理過的數據(如適用)
│ ├── 📂 raw/
│ ├── 📂 processed/
│ └── ....
├── 📄 .gitignore # Git 忽略的檔案
├── 📄 pyproject.toml # 專案配置(Poetry, setuptools)
├── 📄 uv.lock # UV 檔案
├── 📄 README.md # 主要專案文檔
├── 🐍 setup.py # 安裝腳本(如適用)
├── 📄 LICENSE # 專案許可證
├── 📄 Makefile # 自動化常見任務
├── 📄 DockerFile # 創建 Docker 映像
├── 📂 .github/ # GitHub Actions 工作流程(CI/CD)
│ ├── 📂 actions/
│ └── 📂 workflows/
在這兩種佈局之間的選擇取決於專案的複雜性和長期目標。對於生產質量的程式碼,通常建議使用 src/ 佈局,而平面佈局則適用於簡單或短期專案。
你可以想像不同的模板,更好地適應你的使用案例。保持專案的模組化是很重要的。不要猶豫去創建子目錄,將具有相似功能的腳本分組,並將不同用途的腳本分開。良好的程式碼結構確保可讀性、可維護性、可擴展性和重用性,並有助於有效識別和修正錯誤。
Cookiecutter 是一個開源工具,用於從模板生成預配置的專案結構。它特別有助於確保專案的一致性和組織性,特別是在 Python 中,通過從一開始就應用良好的做法。平面佈局和 src 佈局可以使用 UV 工具啟動。
SOLID 原則
SOLID 程式設計是一種基於五個基本原則的軟體開發方法,旨在提高程式碼質量、可維護性和可擴展性。這些原則為開發穩健、靈活的系統提供了清晰的框架。遵循 SOLID 原則可以減少複雜依賴的風險,使測試更容易,並確保應用程式能夠在面對變化時更輕鬆地演變。無論你是在進行單一專案還是大型應用程式,掌握 SOLID 原則都是採用物件導向程式設計最佳實踐的重要一步。
S — 單一責任原則 (SRP)
單一責任原則意味著一個類別/函數只能管理一件事。這意味著它只有一個改變的理由。這使得程式碼更容易維護和閱讀。具有多個責任的類別/函數難以理解,並且常常是錯誤的來源。
範例:
# 違反 SRP
class MLPipeline:
def __init__(self, df: pd.DataFrame, target_column: str):
self.df = df
self.target_column = target_column
self.scaler = StandardScaler()
self.model = RandomForestClassifier()
def preprocess_data(self):
self.df.fillna(self.df.mean(), inplace=True) # 處理缺失值
X = self.df.drop(columns=[self.target_column])
y = self.df[self.target_column]
X_scaled = self.scaler.fit_transform(X) # 特徵縮放
return X_scaled, y
def train_model(self):
X, y = self.preprocess_data() # 在模型訓練中進行數據預處理
self.model.fit(X, y)
print("模型訓練完成。")
在這裡,MLPipeline 類別有兩個責任:生成內容和保存檔案。
# 遵循 SRP
class DataPreprocessor:
def __init__(self):
self.scaler = StandardScaler()
def preprocess(self, df: pd.DataFrame, target_column: str):
df = df.copy()
df.fillna(df.mean(), inplace=True) # 處理缺失值
X = df.drop(columns=[target_column])
y = df[target_column]
X_scaled = self.scaler.fit_transform(X) # 特徵縮放
return X_scaled, y
class ModelTrainer:
def __init__(self, model):
self.model = model
def train(self, X, y):
self.model.fit(X, y)
print("模型訓練完成。")
O — 開放/關閉原則 (OCP)
開放/關閉原則意味著一個類別/函數必須對擴展開放,但對修改關閉。這使得可以添加功能,而不會破壞現有的程式碼。
以這個原則進行開發並不容易,但對於主要開發者來說,一個好的指標是,在專案開發過程中,合併請求中添加的內容 (+) 越來越多,而刪除的內容 (-) 越來越少。
L — 里斯科夫替代原則 (LSP)
里斯科夫替代原則指出,子類別可以替代其父類別,而不改變程式的行為,確保子類別滿足基類定義的期望。這限制了意外錯誤的風險。
範例:
# 違反 LSP
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
# 改變正方形的寬度違反了正方形的定義。
為了遵守 LSP,最好避免這種層級結構,使用獨立的類別:
class Shape:
def area(self):
raise NotImplementedError
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
I — 介面隔離原則 (ISP)
介面隔離原則指出,應該建立幾個小類別,而不是一個包含無法在某些情況下使用的方法的大類別。這樣可以減少不必要的依賴。
範例:
# 違反 ISP
class Animal:
def fly(self):
raise NotImplementedError
def swim(self):
raise NotImplementedError
最好將 Animal 類別拆分為幾個類別:
# 遵循 ISP
class CanFly:
def fly(self):
raise NotImplementedError
class CanSwim:
def swim(self):
raise NotImplementedError
class Bird(CanFly):
def fly(self):
print("飛翔")
class Fish(CanSwim):
def swim(self):
print("游泳")
D — 依賴反轉原則 (DIP)
依賴反轉原則意味著一個類別必須依賴於抽象類別,而不是具體類別。這樣可以減少類別之間的連接,使程式碼更具模組化。
範例:
# 違反 DIP
class Database:
def connect(self):
print("連接到資料庫")
class UserService:
def __init__(self):
self.db = Database()
def get_users(self):
self.db.connect()
print("獲取用戶")
在這裡,UserService 的 db 屬性依賴於 Database 類別。為了遵守 DIP,db 必須依賴於一個抽象類別。
# 遵循 DIP
class DatabaseInterface:
def connect(self):
raise NotImplementedError
class MySQLDatabase(DatabaseInterface):
def connect(self):
print("連接到 MySQL 資料庫")
class UserService:
def __init__(self, db: DatabaseInterface):
self.db = db
def get_users(self):
self.db.connect()
print("獲取用戶")
# 我們可以輕鬆更改使用的資料庫。
db = MySQLDatabase()
service = UserService(db)
service.get_users()
PEP 標準
PEP(Python 增強提案)是技術性和資訊性文件,描述了 Python 社群的新功能、語言改進或指導方針。其中,PEP 8 定義了 Python 程式碼的風格慣例,對於促進專案的可讀性和一致性起著根本性作用。
採用 PEP 標準,特別是 PEP 8,不僅確保程式碼對其他開發者可理解,還確保其符合社群設定的標準。這促進了協作、重讀和長期維護。
在這篇文章中,我將介紹 PEP 標準中最重要的幾個方面,包括:
- 風格慣例(PEP 8):縮排、變數名稱和導入組織。
- 文檔編寫的最佳實踐(PEP 257)。
- 編寫類型化、可維護程式碼的建議(PEP 484 和 PEP 563)。
理解和應用這些標準對於充分利用 Python 生態系統並貢獻於專業質量的專案至關重要。
PEP 8
這份文檔是關於編碼慣例的,以標準化程式碼,PEP 8 有很多文檔。我不會在這篇文章中展示所有建議,只會展示我認為在審查程式碼時至關重要的部分。
命名慣例
變數、函數和模組名稱應使用小寫,並用底線分隔單詞。這種排版慣例稱為 snake_case。
my_variable
my_new_function()
my_module
常數應以大寫字母書寫,並在腳本開頭(導入之後)設置:
LIGHT_SPEED
MY_CONSTANT
最後,類別名稱和例外使用 CamelCase 格式(每個單詞開頭的字母大寫)。例外必須在結尾包含 Error。
MyGreatClass
MyGreatError
記得給你的變數取有意義的名稱!不要使用像 v1、v2、func1、i、toto 這樣的變數名稱。
單字符變數名稱在循環和索引中是允許的:
my_list = [1, 3, 5, 7, 9, 11]
for i in range(len(my_list)):
print(my_list[i])
一種更「Pythonic」的寫法,應優先於上述範例,去掉 i 索引:
my_list = [1, 3, 5, 7, 9, 11]
for element in my_list:
print(element)
空格管理
建議在運算符(+、-、*、/、//、%、==、!=、>、not、in、and、or 等)前後加上空格:
# 建議的程式碼:
my_variable = 3 + 7
my_text = "mouse"
my_text == my_variable
# 不建議的程式碼:
my_variable=3+7
my_text="mouse"
my_text==my_variable
你不能在運算符周圍添加多個空格。另一方面,方括號、大括號或圓括號內部不應有空格:
# 建議的程式碼:
my_list[1]
my_dict{"key"}
my_function(argument)
# 不建議的程式碼:
my_list[ 1 ]
my_dict{ "key" }
my_function( argument )
在字符「:」和「,」之後建議加一個空格,但不應在之前加空格:
# 建議的程式碼:
my_list= [1, 2, 3]
my_dict= {"key1": "value1", "key2": "value2"}
my_function(argument1, argument2)
# 不建議的程式碼:
my_list= [1 , 2 , 3]
my_dict= {"key1":"value1", "key2":"value2"}
my_function(argument1 , argument2)
然而,在索引列表時,我們不會在「:」後加空格:
my_list= [1, 3, 5, 7, 9, 1]
# 建議的程式碼:
my_list[1:3]
my_list[1:4:2]
my_list[::2]
# 不建議的程式碼:
my_list[1 : 3]
my_list[1: 4:2 ]
my_list[ : :2]
行長度
為了可讀性,我們建議程式碼行長度不超過 80 個字符。然而,在某些情況下,這條規則可以被打破,尤其是當你在進行 Dash 專案時,遵守這個建議可能會很困難。
可以使用「\」字符來切割過長的行。
例如:
my_variable = 3
if my_variable > 1 and my_variable < 10 \
and my_variable % 2 == 1 and my_variable % 3 == 0:
print(f"我的變數等於 {my_variable }")
在括號內,你可以在不使用「\」字符的情況下換行。這對於指定函數或方法的參數時非常有用:
def my_function(argument_1, argument_2,
argument_3, argument_4):
return argument_1 + argument_2
也可以通過在逗號後跳過一行來創建多行列表或字典:
my_list = [1, 2, 3,
4, 5, 6,
7, 8, 9]
my_dict = {"key1": 13,
"key2": 42,
"key2": -10}
空白行
在腳本中,空白行對於視覺上分隔程式碼的不同部分非常有用。建議在函數或類別的定義之前留兩個空白行,在方法(在類別中)定義之前留一個空白行。你也可以在函數的主體中留一個空白行,以分隔函數的邏輯部分,但這應該儘量少用。
註解
註解總是以 # 符號開頭,後面跟著一個空格。它們清楚地解釋了程式碼的目的,並且必須與程式碼保持同步,即如果程式碼被修改,註解也必須相應修改(如適用)。它們與所註解的程式碼在同一縮排級別。註解應為完整的句子,開頭使用大寫字母(除非第一個單詞是變數,則不需要大寫),並在結尾加上句號。我強烈建議用英語撰寫註解,並且在註解所用語言和變數命名所用語言之間保持一致。最後,應盡量避免在同一行程式碼後面跟註解,並且應與程式碼至少隔開兩個空格。
幫助你的工具
Ruff 是一個用 Rust 編寫的 Python 程式碼分析工具和格式化工具。它結合了 flake8 linter 和 black 及 isort 格式化的優點,並且速度更快。
Ruff 在 VS Code 編輯器上有擴展。
要檢查你的程式碼,你可以輸入:
ruff check my_module.py
但也可以用以下命令進行修正:
ruff format my_module.py
PEP 20
PEP 20:Python 的禪是一組以詩意形式寫成的 19 條原則。它們更像是一種編碼方式,而不是實際的指導方針。
美比醜好。明確比隱含好。簡單比複雜好。複雜比困難好。平坦比嵌套好。稀疏比密集好。可讀性很重要。特殊情況不夠特殊到可以打破規則。雖然實用性勝過純粹性。錯誤不應該默默無聞。除非明確地靜默。在面對模糊時,拒絕猜測的誘惑。應該有一種——而且最好只有一種——明顯的方式來做到這一點。雖然這種方式一開始可能不明顯,除非你是荷蘭人。現在比永遠好。雖然永遠通常比「現在」更好。如果實現難以解釋,那就是壞主意。如果實現容易解釋,那可能是好主意。命名空間是一個偉大的主意——讓我們多做一些!
PEP 257
PEP 257 的目的是標準化文檔字符串的使用。
什麼是文檔字符串?
文檔字符串是出現在函數、類別或方法定義後的第一條指令中的字符串。文檔字符串成為該對象的 __doc__ 特殊屬性的輸出。
def my_function():
"""這是一個文檔字符串。"""
pass
我們可以這樣獲取:
>>> my_function.__doc__
>>> '這是一個文檔字符串。'
我們總是用三個雙引號 “”” 來撰寫文檔字符串。
單行文檔字符串
用於簡單的函數或方法,必須放在單行內,開頭和結尾都不能有空白行。結束引號與開頭引號在同一行,文檔字符串前後都沒有空白行。
def add(a, b):
"""返回 a 和 b 的總和。"""
return a + b
單行文檔字符串不得重新整合函數/方法的參數。不要這樣做:
def my_function(a, b):
""" my_function(a, b) -> list"""
多行文檔字符串
第一行應該是對被文檔化對象的總結。接著是一個空行,然後是對參數的更詳細解釋或說明。
def divide(a, b):
"""將 a 除以 b。
返回除法的結果。如果 b 等於 0,則引發 ValueError。
"""
if b == 0:
raise ValueError("只有查克·諾里斯可以除以 0")
return a / b
完整文檔字符串
完整的文檔字符串由幾個部分組成(在這裡,基於 numpydoc 標準)。
- 簡短描述:總結主要功能。
- 參數:描述參數及其類型、名稱和角色。
- 返回:指定返回值的類型和角色。
- 引發:記錄函數引發的例外。
- 附註(可選):提供額外的說明。
- 範例(可選):包含帶有預期結果或例外的示範用法。
def calculate_mean(numbers: list[float]) -> float:
"""
計算數字列表的平均值。
參數
----------
numbers : list of float
要計算平均值的數值列表。
返回
-------
float
輸入數字的平均值。
引發
------
ValueError
如果輸入列表為空。
附註
-----
平均值計算為所有元素的總和除以元素的數量。
範例
--------
計算數字列表的平均值:
>>> calculate_mean([1.0, 2.0, 3.0, 4.0])
2.5
幫助你的工具
VsCode 的 autoDocstring 擴展可以自動創建文檔字符串模板。
PEP 484
在某些程式語言中,聲明變數時必須指定類型。在 Python 中,類型是可選的,但強烈建議使用。PEP 484 為 Python 引入了一個類型系統,對變數、函數參數和返回值進行類型註解。這個 PEP 為提高程式碼可讀性、促進靜態分析和減少錯誤提供了基礎。
什麼是類型註解?
類型註解是明確聲明變數的類型(浮點數、字符串等)。typing 模組提供了定義通用類型的標準工具,如 Sequence、List、Union、Any 等。
要對函數屬性進行類型註解,我們使用「:」來表示函數參數的類型,使用「->」來表示返回值的類型。
以下是一些未進行類型註解的函數:
def show_message(message):
print(f"Message : {message}")
def addition(a, b):
return a + b
def is_even(n):
return n % 2 == 0
def list_square(numbers):
return [x**2 for x in numbers]
def reverse_dictionary(d):
return {v: k for k, v in d.items()}
def add_element(ensemble, element):
ensemble.add(element)
return ensemble
現在這些函數應該這樣寫:
from typing import List, Tuple, Dict, Set, Any
def show_message(message: str) -> None:
print(f"Message : {message}")
def addition(a: int, b: int) -> int:
return a + b
def is_even(n: int) -> bool:
return n % 2 == 0
def list_square(numbers: List[int]) -> List[int]:
return [x**2 for x in numbers]
def reverse_dictionary(d: Dict[str, int]) -> Dict[int, str]:
return {v: k for k, v in d.items()}
def add_element(ensemble: Set[int], element: int) -> Set[int]:
ensemble.add(element)
return ensemble
幫助你的工具
MyPy 擴展會自動檢查變數的使用是否符合聲明的類型。例如,對於以下函數:
def my_function(x: float) -> float:
return x.mean()
編輯器將指出浮點數沒有「mean」屬性。
這樣的好處是雙重的:你將知道聲明的類型是否正確,以及這個變數的使用是否符合其類型。
在上述範例中,x 必須是具有 mean() 方法的類型(例如 np.array)。
結論
在這篇文章中,我們探討了創建乾淨 Python 生產程式碼的最重要原則。穩固的架構、遵循 SOLID 原則以及遵守 PEP 建議(至少是這裡討論的四個)對於確保程式碼質量至關重要。對於美麗程式碼的渴望並不是(僅僅是)矯情。它標準化了開發實踐,使團隊合作和維護變得更加容易。沒有什麼比花費數小時(甚至數天)逆向工程一個程式、解讀寫得不好的程式碼,然後才能最終修復錯誤更令人沮喪的了。通過應用這些最佳實踐,你可以確保你的程式碼保持清晰、可擴展,並且未來任何開發者都能輕鬆使用。
參考資料
1. src 佈局與平面佈局
2. SOLID 原則
3. Python 增強提案索引
本文由 AI 台灣 運用 AI 技術編撰,內容僅供參考,請自行核實相關資訊。
歡迎加入我們的 AI TAIWAN 台灣人工智慧中心 FB 社團,
隨時掌握最新 AI 動態與實用資訊!