iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0

本篇目的:讀者可以從零開始,以”目標設定→流程設計→定義手法”的方式,做出一個擁有完整功能、易修改的模組。

前言

在 day 14 中我們知道如何以 class 的方式管理 data pipline,但 class 還有另一個使用方式:建立獨立的功能模組。例如前面 day 3 ~ day 8 一直在使用的 pd.Dataframe 就是一個為了整理表格資料所設計出來的 class,這個 class 當然也會實作出 sort_values 、 pivot_table 等表格相關的 method。而我們今天要做的便是這樣的應對特定情境的 class。

在現在 2026 年,絕大部分的模組使用方法都可以在 Google 或各種生成式 AI 中的對話中取得。因此比起知道模組的各種功能如何叫出來,我們更應該知道我們要做什麼、然後需要設計出甚麼功能。模組設計就像準備工具箱一樣:知道工具箱的應用情境、準備對應的工具、並且在需要時將工具箱內的工具拿出來用。

因此在今天的文章中,我們就要來打造自己的工具箱。我們會以 day 17 的設定檔模組開始,以我們平常做專案規劃的方式,從”目標回推到手法”來設計模組。並且示範以心智圖、class diagram 等方式繪製出結構。藉由此流程不僅可以建立**「程式架構」**的概念,還能將這些圖表交給 AI 生成模組出來。

設定 class 的架構與預期使用方式

在實際編寫 class 前,一定要先知道”這個 class 預計要有甚麼功能,我應該做些甚麼才能達成這些功能”。以完成一個 json 管理 class 為例,它應該要能夠管理 day 17 提到的系統設定檔、使用者設定檔,也要設計讓使用者容易讀取 SPEC 的方式。藉由從大目標到實際手法的設計,我們可以知道這個 class 要實際做出哪些 method。

我們知道了目的,就可以先來想一個好用的 class 需要有哪些功能並將其列出來。以這個 class 來說,我希望有以下功能:

  • User 在實際應用情境不同下 (只有使用者設定檔/ 只有系統設定檔 / 都有),都可以用此 class 管理設定檔資料

  • User 在實例化這個 class 後,可以簡單的讀取對應設定檔資料

  • 可以指定設定檔內容作新增/刪除等修改

對應上面的功能,會有以下的細節

  • Class 內部應該要針對 "沒有對應設定檔 ” 的情況下自動輸出空 dict

  • 讀取設定檔資訊的方式跟 dict 類似

  • 要設計修改設定檔內容的方法,須同步更新物件內資訊與 json 檔

下面的 mindmap 便示範了這樣的流程。中間的雲是這個 class 的目的,雲周圍的節點是這個 class 要有的功能。我們一路從目的->功能繼續寫下去,最後可以看到尾端就是我們要實作之 method。

https://ithelp.ithome.com.tw/upload/images/20261001/20182319Iu9787W9ud.png

確認了應該要實作的 method 後,我們可以規劃這個 class 的實際架構,例如 property 等。以下使用了 class diagram 來表示這個 class 的組成,上面用粗體標示的是 class 的名稱、中間的格子是該 class 會有的 property,下面的格子是這個 class 會設定的方法。

https://ithelp.ithome.com.tw/upload/images/20261001/20182319N0FV6k2O2j.png

這些圖表使用 Mermaid 繪製,大家可以到這個網站試試看。使用 Mermaid 可以讓生成式 AI 更加了解我們需要的程式結構

應用例:管理 json 設定檔的 class 功能實作

藉由上面的思考方式與圖表輔助,我們 (又或是接下來要開始幹活的生成式 AI) 對於 class 功能與所需架構應有清楚的認知。以下的示範例便是根據上面的圖表實作出 method。

Class 結構定義請見 day 14,今日文章主要說明 class 架構的思考流程

初始化時檢查與建立 json 檔案

我們的目的是 class 實例化成物件後,user 就可以直接讀取裡面存下來的設定檔資訊,因此我們會在 __init__ 內設定 ”初始化使用者設定資料夾 → 初始化使用者設定檔案 → 讀取 json 資料到記憶體” 的流程。

而我們在 mindmap 中有提到 “若 user 有傳入使用者設定檔名稱,就要保證這些檔案存在“ (也就是檢查與初始化),因此若 user_config_name 有傳入,就會執行 ”初始化使用者設定資料夾 → 初始化使用者設定檔案” 這個流程。

class JsonManager:
    def __init__(self, project_name: str, user_config_name: list[str] = None):
        # 依據 Class Diagram 定義私有屬性
        self._project_name = project_name
        self._user_config_name = user_config_name if user_config_name else []
        
        # 公開的記憶體資料緩衝區
        self.sys_config: dict | None = None
        self.user_config: dict[str, dict[str,list[dict[str, Any]]]] = {}
        
        # 1. 初始化使用者設定檔與資料夾
        if self._user_config_name:
            self._init_user_config(self._user_config_name)
            
        # 2. 自動載入設定檔至記憶體
        self._load_sys_config()
        self._load_user_config()

    def _get_app_data_dir(self) -> Path:
        """取得使用者設定資料夾路徑 (內部使用,使用 _project_name)"""
        return Path(os.environ.get("APPDATA", ".")) / self._project_name

    def _init_user_config(self, name_list: list[str]):
        """檢查並初始化使用者設定資料夾與各個 json 檔案"""
        app_data_dir = self._get_app_data_dir()
        
        # 步驟一:檢查資料夾是否存在,不存在則遞迴建立
        if not app_data_dir.exists():
            app_data_dir.mkdir(parents=True)
            print(f"已建立設定檔資料夾:{app_data_dir}")
            
        # 步驟二:檢查清單內的 json 檔案,不存在則寫入空字典 ({})
        for name in name_list:
            file_path = app_data_dir / f"{name}.json"
            if not file_path.exists():
                with open(file_path, "w", encoding="utf-8") as f:
                    json.dump({}, f, ensure_ascii=False, indent=4)
                print(f"已建立預設設定檔:{file_path.name}")

讀取 json

初始化使用者路徑與檔案後,就會開始讀取 json 檔案內的內容。為了使用方便, 在 __init__ 中就會直接呼叫讀取 method,實例化後就會直接讀檔。

針對不同設定檔,我們設計以下兩種存取方式:

  1. 系統設定檔:user 直接讀取物件 property 即可 ( json_manager.sys_config )

  2. 使用者設定檔:定義一個 get_user_spec method,讓 user 可以傳入 SPEC 名稱後讀取所需資料。

get_user_spec 中不直接使用 get 方法、並且另外加上檢查的原因是讓 user 可以第一時間發現錯誤。避免 user 打錯名稱後”正常”回傳空 dict,後續使用者就算發現資料有問題,也很難發現自己打錯名稱。

這種設計方式稱為 Fail-Fast (快速失敗),錯誤會在發生時就發出通知而不是勉強執行。就像應該要在製程各站的檢查發現製程的異常,而不是到最後檢測時才發現

    def _get_resource_path(self, relative_path: str) -> Path:
        """取得系統設定檔的絕對路徑 (相容開發環境與 PyInstaller 打包後的 exe)"""
        if hasattr(sys, "_MEIPASS"):
            base_path = Path(sys._MEIPASS)
        else:
            base_path = Path(__file__).resolve().parent
        return base_path / relative_path

    def _load_sys_config(self):
        """讀取系統設定檔並寫入 sys_config 屬性 (固定讀取 config.json)"""
        file_path = self._get_resource_path("config.json")
        if file_path.exists():
            with open(file_path, "r", encoding="utf-8") as f:
                self.sys_config = json.load(f)
        else:
            self.sys_config = {}

    def _load_user_config(self):
        """讀取所有指定的使用者設定檔並寫入 user_config 屬性"""
        if not self._user_config_name:
            return

        app_data_dir = self._get_app_data_dir()
        for name in self._user_config_name:
            file_path = app_data_dir / f"{name}.json"
            if file_path.exists():
                with open(file_path, "r", encoding="utf-8") as f:
                    self.user_config[name] = json.load(f)

    def get_user_spec(self, spec_name: str) -> dict:
        """使用者可以傳入名稱後得到對應資料,沒有設定檔的話會跳出 error"""
        if spec_name in self._user_config_name:
            return self.user_config.get(spec_name)
        else:
            raise ValueError(f'無此SPEC設定檔: {spec_name}')

將設定寫入 json

使用者設定可能會在使用過程中修改 (例如新增一個產品的 SPEC),因此我們也要做出一個寫入 SPEC 到物件以及檔案的方法。以下方法 save_user_config 不僅會更新記憶體中的 user_config ,還會透過 json.dump 將資料檔案寫入到 json 檔案中。

實際上使用時我們可以參照 day 16 的連接方式,在 controller 中連接此 method 與UI 元件。當 user 在 UI 上點擊「儲存設定」按鈕時,我們會將資料打包並傳入 save_user_config。

    def save_user_config(self,spec_name: str ,new_item_name: str, data: dict):
        """
        將新資料更新到 class property 與指定的設定檔內
        """
        # 防呆檢查:若傳入未定義的設定檔名稱則及早報錯
        if spec_name not in self._user_config_name:
            raise ValueError(f"無此 SPEC 設定檔:{spec_name}")

        # 擷取出要編輯的 spec 並寫入
        new_data = self.user_config[spec_name]
        new_data[new_item_name] = data
        
        # 更新資料到 class property 內
        self.user_config[spec_name] = new_data
        # 寫入實體檔案:將該 spec 完整的內容寫入對應的 json 檔案
        file_path = self._get_app_data_dir() / f"{spec_name}.json"
        with open(file_path, "w", encoding="utf-8") as f:
            json.dump(new_data, f, ensure_ascii=False, indent=4)
        print(f"已成功更新並儲存設定至:{file_path.name}")

將讀取的設定檔資料顯示於 UI 上

以下示範如何應用這個 class。首先我們先回到 controller 並在 __init__ 內並實例化 JsonManager ,我們就可以讀取 self.json_manager 內的資料取得設定,並且在 中指派 UI 的修改與新資訊的寫入。

這裡展現了核心架構的關注點分離:UI 只管呈現、JsonManager 只管設定檔的讀寫與本機路徑,而 controller 負責調度。

# 示範在 Main Controller 中整合 JsonManager 與 UI
from PySide6.QtWidgets import QMainWindow
# from ui_main import Ui_MainWindow   # Qt Designer 轉換出來的 UI 類別
# from config_manager import JsonManager # 剛剛完成的 JsonManager 模組

class MainController(QMainWindow):
    def __init__(self):
        super().__init__()
        # self.ui = Ui_MainWindow()
        # self.ui.setupUi(self)
        
        # 建立 JsonManager 實例
        self.json_manager = JsonManager(
            project_name="MyWaferTool", 
            user_config_name=["user_spec", "last_status"]
        )
        
        # 讀取系統設定 (config.json) 並填入下拉選單
        sys_settings = self.json_manager.system_settings
        if "machine_list" in sys_settings:
            self.ui.combo_machine.addItems(sys_settings["machine_list"])

        # 連接按鍵到設定檔寫入功能
        self.ui.a_button.clicked.connect(self.add_data_to_user_spec)

    def add_data_to_user_spec(self):
        """將UI上的SPEC輸入寫入到設定檔中"""
        
        # 讀取相關設定
        new_item_name = self.ui.a_lineedit.text()
        # data = self...... (讀取使用者輸入)

        # 寫入到管理器之中
        self.json_manager.save_user_config("user_spec", new_item_name, data)

To be continued →

我們今天學會了 class 設計的方式,並且實做了一個設定檔管理模組。

在明天 day 19 的教學中,我們會用與今天相同的 class 設計方式,來實作 day 11 沒有說明的 QTableWidget 讀寫方式。在這個 class 中,我們會將 QTableWidget 會用到的功能,例如讀取資料並轉成適當格式、增加表格的欄/列、寫入資料到表格中,都整合到單一 class 中。


上一篇
Day 17: 不用吃銀杏也有好記憶 - 使用 json 模組讓 GUI 程式能留存使用者資料
系列文
用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言