13 min給工具使用者

從一段 Python 走到一個可重跑的小專案

把 Python 程式拆成可重跑的小專案:固定入口、輸入、模組責任、JSON 持久化與 README,並用新程序驗證 import、物件狀態與輸出結果。

Aaron Huang系統、產品與 AI 實作

從一段 Python 走到一個可重跑的小專案

把一段 Python 變成「可重跑的小專案」,最低限度要固定五件事:入口、輸入、處理責任、持久化結果、重跑方式。 檔案變多本身沒有價值;真正有價值的是另一個新程序可以知道從哪裡開始、讀哪份資料、呼叫哪段邏輯、結果寫到哪裡,並照說明得到可預期的結果。

上一篇〈Python 程式怎麼處理資料?〉先建立單一程式裡的資料流;這篇把那條資料流放進兩個 Python 檔案,再加入 JSON 持久化、import、class 與 README。若你連「現在是哪個 Python 在跑、working directory 在哪裡」都還不確定,先回到〈Python 到底在哪裡跑?〉。

本篇只做到:能讀懂、修改並重跑一個 2 個 Python 檔案的小專案。 不進 packaging、部署、Git、HTTP、資料庫或大型架構。

小型 Python 專案圖解:磁碟上的輸入、原始碼與輸出,和程序內 NoteSelector instance 的單一路徑

目錄

可重跑的小專案,最低要固定哪五件事?

先看答案:

要固定的事 本篇範例
入口 main.py
輸入 data/notes.json
處理責任 helpers.py 裡的 NoteSelector
持久化結果 output/selected.json
重跑方式 README 裡的 Python 版本、必要檔案、指令與預期結果

最小結構只有:

note-project/
├── main.py
├── helpers.py
├── data/
│   └── notes.json
├── README.md
└── output/
    └── selected.json   # 成功執行後產生

這裡不先建立 services/、controllers/ 或其他空架構層。只要說不清楚「為什麼需要這個檔案」,就不因為看起來更像大型專案而加入。

程式關掉後,資料怎麼留下?

Python 裡的 list、dict、class instance 都屬於目前程序的記憶體狀態。程序結束後,下一個 Python 不會自動取得那些物件。

要跨程序留下資料,本篇採最直接的方法:寫進磁碟檔案。

東西 關掉 Python 後 本篇用途
list / dict / instance 不會自動保留 程序內運算與設定
JSON 檔案 仍在磁碟上 保存輸入與結果
README 仍在磁碟上 保存重跑條件與步驟

資料會經過:

JSON 檔
→ json.load()
→ Python 資料
→ NoteSelector 處理
→ json.dump()
→ 新 JSON 檔

檔案模式也要分清楚:

模式 核心行為
r 讀取既有檔案
w 開啟既有檔案時先截斷,再進行後續寫入
a 從既有檔案尾端追加

所以原始輸入和可重建輸出要分開。用 w 寫結果不是「自動安全替換」,後續寫入若失敗,舊內容不會自己恢復。文字讀寫也明確指定 UTF-8,不假設作業系統預設編碼都一樣。

main.py 和 helpers.py 怎麼分工?

這個教學例子有四筆合成資料:

[
  {"title": "整理 Python 路徑", "minutes": 25, "done": true},
  {"title": "複習 return", "minutes": 10, "done": true},
  {"title": "練習 JSON 讀寫", "minutes": 30, "done": false},
  {"title": "確認 README 重跑", "minutes": 20, "done": true}
]

篩選規則只有兩個:

  • done = true
  • minutes >= 20

因此預期留下「整理 Python 路徑」與「確認 README 重跑」。

helpers.py 只負責篩選:

class NoteSelector:
    def __init__(self, min_minutes, done_only=True):
        self.min_minutes = min_minutes
        self.done_only = done_only

    def select(self, notes):
        selected = []
        for note in notes:
            if self.done_only and not note["done"]:
                continue
            if note["minutes"] >= self.min_minutes:
                selected.append({
                    "title": note["title"],
                    "minutes": note["minutes"],
                })
        return selected

正文用一個最小版 main.py 把流程串起來:

import json
from pathlib import Path
from helpers import NoteSelector

ROOT = Path(__file__).resolve().parent
INPUT = ROOT / "data" / "notes.json"
OUTPUT = ROOT / "output" / "selected.json"


def main():
    with INPUT.open("r", encoding="utf-8") as file:
        notes = json.load(file)

    selector = NoteSelector(20, True)
    selected = selector.select(notes)

    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    with OUTPUT.open("w", encoding="utf-8") as file:
        json.dump(selected, file, ensure_ascii=False, indent=2)

    print(f"read={len(notes)} selected={len(selected)}")


if __name__ == "__main__":
    main()

這個最小版和後面的 __name__ 說明使用同一個 execution model:直接執行 main.py 才呼叫主流程;只做 import main 不會產生輸出。

第一次執行 python main.py 後,不要只看「沒有報錯」。至少確認:

  1. 顯示 read=4 selected=2。
  2. 原始 data/notes.json 仍是四筆。
  3. output/selected.json 只有預測的兩筆。

接著關掉前一個 Python,再開新的程序重新讀 output/selected.json。這一步成功,才直接證明結果真的留在磁碟,而不是只存在前一個程序的變數裡。

import 到底做了什麼?

from helpers import NoteSelector 不等於「把另一個檔案的文字貼進 main.py」。

夠用的心智模型是:

  1. Python 先找到 helpers module。
  2. module 有自己的 namespace,也就是名稱與物件的對應範圍。
  3. module 第一次載入時,頂層敘述會被處理。
  4. from helpers import NoteSelector 把 NoteSelector 這個名稱綁到目前 module 可使用的 namespace。

如果寫 import helpers,後面會用 helpers.NoteSelector(...);差別是名稱怎麼進入目前程式,不是 class 變成另一份。

本篇 main.py 最後使用 if __name__ == "__main__": main()。這個 guard 只控制「要不要呼叫主流程」,不是讓整個檔案在 import 時完全停止處理。

遇到「明明有 helpers.py 卻 import 不到」,不要先改 sys.path 或安裝不明套件。先確認啟動位置,再看 helpers.__file__ 指向哪一份檔案。

做一次刻意失敗就夠:先在 note-project 內執行 python -c "import helpers; print(helpers.__file__)",確認載入本專案的檔案;再移到父資料夾執行 python -c "import helpers"。在沒有其他同名 module 的情況下,預期得到 ModuleNotFoundError。回到 note-project 後,同一個 import 應恢復成功。這個「失敗 → 看搜尋位置 → 回到正確位置」就是本篇的 controlled failure,不需要在 README 或驗收段落再重教一次。

class 的狀態為什麼不等於持久化?

NoteSelector 是 class 定義;NoteSelector(20, True) 才建立 instance。

建立 instance 時,Python 會把新物件當成 self 傳給 __init__(),你提供的 20 與 True 則成為這個 instance 的設定。

同一個 class 可以建立:

  • normal = NoteSelector(20, True)
  • strict = NoteSelector(25, True)

同一份 notes 下,normal 應留下 2 筆,strict 應只留下 1 筆。

呼叫 normal.select(notes) 時,normal 會成為 method 裡的 self,notes 才是你明確傳入的方法資料。

如果寫成 NoteSelector(),必要的 min_minutes 沒有提供,Python 會在呼叫邊界提出 TypeError。最小修正是補上參數,不是重寫 class。

但 normal / strict 的設定仍然只是程序內狀態。關掉 Python 後,它們不會自動被保存;真正跨程序留下來的是磁碟檔案。

出錯時,先判斷是哪一層

不要看到錯誤就全面重寫。先判斷程式在哪一層失敗:

現象 發生層次 先查什麼 最小修正
FileNotFoundError 檔案位置 traceback 裡實際找的路徑 還原檔名或修正路徑
JSON 解碼錯誤 檔案格式 JSON 是否有多餘逗號、缺括號 修正 JSON 語法
minutes 是 "20" 資料契約 外層是 array 後,再看欄位型別 改回整數
ModuleNotFoundError module 搜尋 啟動位置與 helpers.__file__ 回到正確位置/確認載入來源
NoteSelector() 的 TypeError 呼叫參數 缺哪個必要參數 補 min_minutes

另外要留意:如果前一次成功過,後一次卻在讀檔或解析 JSON 時失敗,舊的 output/selected.json 可能還在。舊結果存在,不代表這次成功。 驗證要看這次程序是否成功結束,以及輸出是否真的符合這次輸入與設定。

本篇把 input 和 output 分開,只處理最直接的覆寫風險;它不是備份、版本歷史、檔案鎖或正式資料安全方案。

README 怎樣才真的能讓別人重跑?

README 的目的不是看起來完整,而是消除只有作者知道的隱藏前提。

至少要交代:

問題 本篇答案
用什麼環境? 可用的 Python 3
要裝什麼? 不需第三方套件
哪些檔案不能缺? main.py、helpers.py、data/notes.json
怎麼啟動? python main.py
輸入是什麼? UTF-8 JSON 筆記陣列
預期結果? 4 筆讀入、2 筆輸出
會改什麼? 建立/覆寫 output/selected.json
有什麼限制? 無歷史備份、無檔案鎖、不適合重要資料

最直接的驗收是:關掉原終端機,開一個新的,只照 README 從頭執行。

工作目錄也只需要驗證一次。本篇用 Path(__file__).resolve().parent 當資料路徑基準,所以即使從父資料夾執行 script,輸入與輸出仍應落在同一個專案。若問題是 import 找不到 module,就回到上一節的 helpers.__file__ 診斷,不再重複另一套教學。

怎麼驗收自己真的看懂?

最後做三個小變化,每次都先預測,再看證據:

  1. 把 min_minutes 從 20 改成 25,先預測哪一筆留下,再核對輸出 JSON。
  2. 在 notes.json 增加一筆合成資料,先判斷它會不會被選中,再確認原始四筆沒有意外消失。
  3. 關掉原程序,開新終端機,只照 README 重跑,再重新讀取結果檔。

如果你能沿著:

入口
→ module
→ 檔案讀取
→ Python 資料
→ instance
→ method
→ 檔案寫入
→ 新程序重新讀取

說明每一步由誰負責;遇到錯誤時也能先指出是檔案、資料格式、import 還是呼叫參數問題,再做最小修正,就已經達到這篇的停止線。

停止線與技術來源

本篇來源範圍固定為 AI Engineering Foundations v1.0 的 P05 + S01–S03:

  • P05:檔案、編碼、持久化
  • S01:module、import、namespace、搜尋路徑與入口
  • S02:class、instance、self、__init__ 與物件狀態
  • S03:README、資料、依賴與重現步驟

不進 packaging、deployment、Docker、database、framework architecture、HTTP / external API、Git version control 或 complex inheritance。下一篇才比較 Library、SDK、Framework 與 API 的角色。

技術細節可對照 Python 官方文件:

本篇筆記資料是合成教材,不是外部專案實績。