iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Software Development

用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道系列 第 14 篇

Day 14:一為全,全為一 - 使用 Python class 整合function、引數與結果

  • 分享至 

  • xImage
  •  

本篇目的:了解如何將同一目的之 function 封裝成具備狀態記憶的 Python class,為後續 結合 UI 與模組化設計做準備

前言

我們在 day 8 知道了如何依照 ETL 需求串聯 function,並編寫成一個完整的腳本。腳本執行時會依照編寫順序、從上面執行到下面,因此我們可以很明確的定義出程式執行的方式。這樣的寫法稱為腳本式 (scripting)。

但如果透過 UI 執行 Python 腳本,執行後不會馬上開始運算,而會進入一個無限等待的狀態直到我們呼叫裡面的各種 function(事件迴圈,event loop)。並且通常我們使用 GUI 應用程式時,程式的執行順序會由我們決定,例如開啟 excel 後可能是按新增檔案或是開啟舊檔,這也導致我們在編寫搭配 UI 的腳本時,要做到以下 2 件事:

  • 將執行腳本封裝成可重複使用之 function 或是 class - 在 Day 5、Day 8 與今天的文章有相關說明

  • 定義 UI 元件與腳本內各 function 的連接方式 ,讓使用者可以依照需求呼叫對應的 function,在 Day 16 時說明

Class 具有狀態記憶、可以設定公開介面等優勢,比起純 function 建構的腳本更能與 UI 介面結合。今天會說明如何建立 Python class,為後續的完整 MVC 專案做準備。

Class 是什麼

Class 是 Python 內物件的藍圖 (blueprint),我們可以將 class 想像成產品規格書。產品規格書會描述產品的外觀、應用範圍、性能等,客戶會參照產品規格書與自身需求選用適當的產品並做相關應用。Class 內也會記錄屬性、方法等,我們也是參照設定選用適當 class,並將其實例化成物件。

將同目的的 function 整合成 class 的優勢

Day 8 的腳本式編寫,我們可以想像成手工製作產品,且製作過程全靠經驗判斷,作業完成後沒有留下任何紀錄。而整合成 class 可以想像成自動化機台,我們必須先在機台設定參數與流程,機器會依照設定產出產品且留下製程紀錄。

因此我們將 function 整合成 class 後有以下優勢:

  • 可重複性高:寫成 class 後的使用方式就是 實例化->執行方法,不用再結合多個 func 串成data pipeline,因此適合 import 以及在不同專案重複使用

  • UI、計算與控制模組的分界清晰:由於作業流程已整合在 class 內,因此我們不需要在控制流程的腳本中寫上一堆 function,可以讓流程控制、計算的腳本分責更明確。這部分在 day 15 有更詳細的說明

  • 可以記憶執行結果:可以設定寫入屬性的方法,將 SPEC 設定等屬性寫入到物件內,因此當執行其他 class 的方法時,這個 class 的對應資料也繼續留存

  • 與 UI 介面做結合:GUI 應用程式的運行方式是使用者做出某動作後、執行指定程式。因此我們需要設定不同用途的 class 與方法,對應到不同的 UI 物件

Class 的結構

Class 雖然可以做非常複雜的設定,但 class 最基礎的結構如下

  • 建構式 (constructor):在 class 實例化成物件時會執行的方法,會以 __init__(self, ...) 呈現。可以想像成機台的基礎設定,如真空度等

  • 屬性 / 狀態 (attribute):這個 class 或實例化物件的記錄資料,會以 self.xxx 呈現。能夠透過方法或外部指定來修改與讀取。可以想像成機台執行時,產生的 log。

  • 方法/ 動作 (method): 說明這個 class 或實例化物件可以作業的方式,作業內容通常會牽涉到屬性。會以 def method(self, ...) 呈現。可以想像成機台內描述製程流程的 recipe。

假設我們要做一個 "從指定路徑讀取檢測資料後,作資料清洗並記錄在系統” 的 class,簡單範例如下

# class 名稱通常用大駝峰式命名法
class RawDataHolder:
  
  WAFER_SHAPE = 'circle' # 類別屬性

  def __init__(
    self,
    read_col: list[str] | None = None, 
    skip_row: int | None = None
  ): # 實例化時需傳入之引數

    # 這裡是透過引數設定的屬性
    self.header_row = header_row
    self.read_col = read_col

    # 儲存輸出的屬性,先初始化
    self.dict_wafer_data: dict[str, pd.DataFrame] = {}

  def clean_data(self)
    # 資料清洗流程

  def read_data(self,data_path:Path): # 方法

    # 這裡讀取物件內屬性後執行相關行為
    wafer_id = data_path.stem
    df_a_wafer = pd.read_csv(data_path, skiprows=self.header_row, usecols=self.read_col)

    self.clean_data()

    # 編輯物件的某屬性
    self.dict_wafer_data[wafer_id] = df_a_wafer

類別屬性: class level 的屬性

類別屬性就是定義在類別層級的屬性,例如上面範例的 SHAPE = 'circle' 。類別屬性的特色是所有對應該 class 的物件,都共用同一類別屬性,因此若修改某一物件的類別屬性,所有物件的類別屬性都會變化。

因為共享的特性,類別屬性不會拿來儲存可能會修改之項目,而是用來表示 class 的基本特性,例如 day 16 會講到的 QT 訊號。

# 可以直接透過類別名稱讀取類別屬性
RawDataHolder.SHAPE # circle

# 實例化
wafer_a = RawDataHolder(......)
wafer_b = RawDataHolder(......)

# 修改某物件的類別屬性
wafer_a.WAFER_SHAPE = 'rectangle'

# class 與所有物件的類別屬性都改變了
wafer_a.WAFER_SHAPE # rectangle
wafer_b.WAFER_SHAPE # rectangle
RawDataHolder.SHAPE # rectangle

實體屬性: **__init__ 建構式內的屬性 **

在 __init__ 內的屬性,會在物件實例化時一起建立。不同物件的實體屬性是獨立的,因此像是 self.data_path、self.dict_wafer_data 這類會隨建構式或是方法修改的屬性都會是實體屬性。

我們可以看到在 __init__ 中第一個引數是 self, self 可以想像成”物件本身”。實體屬性是跟著物件走的,在 Python 中我們可以用 物件.屬性 = 修改值 來指定屬性,但在設定 class 時根本還沒實例化成物件,因此 class 內部若牽涉到實體屬性的設置,都會用 self 代稱物件。另外因為類別屬性跟物件無關,因此類別屬性的建立不會出現 self 。

# 未實例化前無法讀取實體屬性
RawDataHolder.dict_wafer_data # 會出錯

# 實例化
wafer_a = RawDataHolder(data_path, skip_row = 7)

# 可以讀取實例化物件的屬性
wafer_a.skip_row # 7
wafer_a.dict_wafer_data # {}

方法 (method)

其實方法就是 class 內的 function。且既然我們都寫成 class 了,當然我們可以使用屬性作為引數,以及透過方法修改屬性。

wafer_a.read_data(a_path) # 執行方法

wafer_a.dict_wafer_data # {wafer id : dataframe} 

示範例:Class 如何讓程式變乾淨

方法與屬性的結合是我們將腳本式的 data pipeline 改寫成 class 的最主要原因,這樣子的結合可以看到以下特點

  1. Data pipeline 全部整理進 RawDataHolder 的 read_data 內了,因此主程式變得易讀

    read_data 就是公開介面,呼叫 read_data 後會自動完成一連串資料讀取流程

  2. 只要物件 wafer_reader 仍存在,隨時可以讀取屬性

  3. 與 UI 連接時,UI 只需呼叫 wafer_reader.read_data ,即可完成一整套資料讀取 + 清洗行為

# 這是主程式
# RawDataHolder 可以寫在其他 py 檔再 import
import RawDataHolder

path_list = [path1, path2]

# 設置資料讀取物件
wafer_reader = RawDataHolder(data_path, skip_row = 7)

for a_path in path_list:

  # 讀寫 + 資料清理
  wafer_reader.read_data(a_path)

# 需要讀取屬性時直接呼叫即可
df_a_wafer = wafer_reader.dict_wafer_data.get(wafer_id)

To be continued →

我們個別知道了計算 class、UI 的建構方式。接下來我們要以專案的角度將這些東西串在一起


上一篇
Day 13: 魔術大空間 - 使用 QT 的多頁面容器做分類與佈局
下一篇
Day 15:程式碼就該像生產線一樣分組 - 談 MVC 架構與關注點分離
系列文
用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言