目錄
一、從“能跑就行”到“能改不怕”——維護性的第一要義
二、單一職責與最小驚訝——維護性的縱深防御
三、可組合的樂高——復用性的第一階梯
四、面向協議設計——復用性的第二階梯
五、異常策略與日志——維護性的隱形護盾
七、測試金字塔——維護性的最后護城河
結語
前言
在軟件工程里,代碼不是寫完即棄的一次性草稿,而是需要持續演進、多人協作、不斷復用的資產。Python 以簡潔語法著稱,但若把簡潔誤當成隨意,很容易寫出“今天能用、明天就炸”的膠水腳本。本文圍繞“維護性”與“復用性”這兩個核心屬性,通過遞進式示例,演示如何把一個 20 行的“一次性腳本函數”逐步重構為可測試、可組合、可演進的通用組件。文章將避免教條式說教,所有結論都源于可運行的代碼片段,讀者可直接粘貼到 REPL 中體驗。
一、從“能跑就行”到“能改不怕”——維護性的第一要義
想象我們接到一個需求:從 CSV 中提取銷售額大于閾值的記錄,并生成 JSON 文件。新手工程師往往一氣呵成:
def do_it():import csv, jsonwith open('sales.csv') as f:rows = [r for r in csv.DictReader(f) if float(r['amount']) > 1000]with open('out.json', 'w') as f:json.dump(rows, f, ensure_ascii=False)
do_it()
這段代碼的問題在于:所有行為都耦合在函數內部。閾值 1000 是硬編碼,文件名寫死,異常被吞掉。三天后產品說“閾值要改成 500”,我們不得不打開源碼、修改字面量、重新部署——維護成本陡增。
維護性的第一步是參數化。把可變部分抽成形參:
import csv
import json
from pathlib import Pathdef extract_big_sales(src: str | Path, dst: str | Path, threshold: float = 1000.0):src, dst = Path(src), Path(dst)with src.open(newline='') as f:rows = [r for r in csv.DictReader(f) if float(r['amount']) > threshold]with dst.open('w', encoding='utf-8') as f:json.dump(rows, f, ensure_ascii=False, indent=2)
現在閾值、路徑都成了可注入的“旋鈕”,單元測試也能輕松覆蓋各種邊界值。注意類型注解與 pathlib 的引入:IDE 能即時提示,跨平臺路徑拼接不再踩坑。
二、單一職責與最小驚訝——維護性的縱深防御
參數化只是起點。隨著需求膨脹,函數仍可能變成“瑞士軍刀”——內部混雜數據讀取、清洗、過濾、序列化等多重職責。一旦某一步驟出錯,定位如同大海撈針。
解決之道是拆分職責。把“讀 CSV”和“轉 JSON”拆成獨立函數,再用一個高階函數把它們串起來:
from typing import Iterable, Dict, Anydef read_sales(src: Path) -> Iterable[Dict[str, Any]]:with src.open(newline='') as f:yield from csv.DictReader(f)def filter_by_threshold(records: Iterable[Dict[str, Any]], threshold: float
) -> Iterable[Dict[str, Any]]:for r in records:if float(r['amount']) > threshold:yield rdef write_json(records: Iterable[Dict[str, Any]], dst: Path) -> None:with dst.open('w', encoding='utf-8') as f:json.dump(list(records), f, ensure_ascii=False, indent=2)
拆分后,每個函數名就是其契約,閱讀者無需深入實現即可理解意圖。更重要的是,它們都返回惰性迭代器,內存占用與源文件大小解耦。
三、可組合的樂高——復用性的第一階梯
復用性并非“復制粘貼”,而是“像積木一樣插拔”。上一步的三個小函數已經具備可組合特性,我們可以用不同的方式拼裝:
def pipeline(src, dst, threshold):write_json(filter_by_threshold(read_sales(src), threshold),dst)
若需求變成“先過濾,再按金額排序,再取前 100 條”,只需在 pipeline 中再插入一個排序步驟即可,其他函數零改動。
四、面向協議設計——復用性的第二階梯
當函數需要適應更多數據源時,可以把“讀操作”抽象成協議。Python 3.8 引入的 Protocol
允許無繼承的結構性子類型:
from typing import Protocol
from io import StringIOclass SalesReader(Protocol):def read(self) -> Iterable[Dict[str, Any]]: ...class CsvSalesReader:def __init__(self, src: Path):self.src = srcdef read(self):with self.src.open(newline='') as f:yield from csv.DictReader(f)class InMemorySalesReader:def __init__(self, text: str):self.text = textdef read(self):yield from csv.DictReader(StringIO(self.text))
現在 filter_by_threshold
不再依賴具體實現,而只依賴 SalesReader
協議。測試時可以注入輕量級的 InMemorySalesReader
,無需觸碰磁盤。
五、異常策略與日志——維護性的隱形護盾
生產代碼需要優雅地失敗。把異常策略收斂到一個地方,避免每個函數都寫重復 try/except:
import logging
from functools import wrapslogging.basicConfig(level=logging.INFO)def log_exceptions(func):@wraps(func)def wrapper(*args, **kwargs):try:return func(*args, **kwargs)except Exception as e:logging.exception(f'{func.__name__} failed: {e}')raisereturn wrapper@log_exceptions
def read_sales(src: Path) -> Iterable[Dict[str, Any]]:...
裝飾器讓橫切關注點(日志、重試、指標)與業務邏輯解耦,后期若接入 Prometheus、Sentry 只需再加一層裝飾器。
六、可演進的配置——復用性的第三階梯
閾值、輸出格式、字段映射,這些都會隨業務變化。硬編碼會讓函數每次需求變更都“動刀”。使用 Pydantic 定義配置模型,把“可變”徹底外部化:
from pydantic import BaseModel, Fieldclass Config(BaseModel):src: Pathdst: Paththreshold: float = Field(gt=0)top_n: int | None = Nonesort_desc: bool = Truedef run(cfg: Config):records = read_sales(cfg.src)records = filter_by_threshold(records, cfg.threshold)if cfg.top_n:records = sorted(records, key=lambda r: float(r['amount']),reverse=cfg.sort_desc)[:cfg.top_n]write_json(records, cfg.dst)
現在函數簽名只剩一個 Config
,所有細節都可由 YAML/JSON/TOML 注入。CI/CD 只需替換配置文件即可實現灰度發布。
七、測試金字塔——維護性的最后護城河
拆分后的純函數天然易于單元測試:
from pytest import fixture@fixture
def sample():return [{'id': 1, 'amount': '900'},{'id': 2, 'amount': '1100'},{'id': 3, 'amount': '2000'},]def test_filter(sample):assert [r['id'] for r in filter_by_threshold(sample, 1000)] == [2, 3]
協議抽象后,測試還能用假對象(Fake)或樁(Stub)實現毫秒級反饋,無需啟動數據庫或文件系統。測試越輕,重構越敢下手。
結語
函數的維護性與復用性不是“后期優化”的奢侈品,而是從第一行代碼就開始積累的資產。通過參數化、職責拆分、協議抽象、配置外部化、異常收斂和測試保障,我們讓函數從“一次性腳本”成長為“可演進的組件”。Python 的動態特性給了我們極大的靈活性,而真正的工程素養體現在:利用這種靈活性去構建“明天還能睡得著”的系統。愿你的每一行代碼,都像樂高積木一樣,既可獨立把玩,又能無限拼接。