--- tags: Python --- # clean code in python ## Design by Contract(DbC) 依合約設計原則 合約:API 預期的輸出、輸入、副作用都該被文件記錄下來,這個文件就是合約,若有不合合約的例外情況,需要用錯誤處理報錯,不應該出現沈默的錯誤 + precondition(前置條件): 驗證呼叫者所給的參數、提供的資料,在呼叫前做的檢查,對於呼叫者作出限制 + postcondition(後置條件): 驗證呼叫者對該函數的期望 + invariant(不變量): optional, 最好在函數的docstring中紀錄不變量,這是在函數執行過程中保持不變的事物。 + side effect(副作用): optional, 在docstring中提及副作用 #### 依合約設計的好處:錯誤更容易被抓出,可驗證。 設計建議: 程式碼分離,一部分程式碼負責前置條件,一部分程式碼負責後置條件,核心函數獨立出來,更利於檢查。 ## Defensive design 防禦性程式設計 針對使用者可能出現的錯誤使用方式提供錯誤處理 作法: + 值的置換 當程式出現錯誤且可能導致軟體完全失敗時,我們可以用相對安全的值來取代原來的結果,一個簡單的例子就是提供參數的預設值來處理未給參數的情況。 + 錯誤紀錄 + 例外處理 例外通常是通知呼叫者出現問題時使用,如果是關係到核心邏輯的錯誤,就直接讓程式失敗,以確保不會有錯誤的執行。 \*\*不要向終端使用者公開traceback,以免安全疑慮\*\* + 避免 broad except except 應該指定要處理的例外,不要留空,這會沈默的錯誤。 拋出例外時,連原始例外一起給 ## Seperation of Concerns 關注點分離 程式的每個部分只負責一小部分的功能,這些功能稱為關注點(Concern) 分離關注點可以減少漣漪效應(ripple effect)這是指軟體從某個起始點開始,因某個改變的錯誤而造成一連串錯誤與例外。 每個關注點都可以由"合約"強制執行。 + cohesion 內聚 表示物件應具有較小且明確定義的用途,而且執行的內容越少越好。 + coupling 耦合 只兩個或多個物件如何依賴彼此,這種依賴構成了限制。 良好軟體的標準: 高內聚,低耦合 ## DRY(Don't Repeat Yourself) 程式碼中的知識只需定義一次,當你必須更改程式碼時,應該只有一個正確的位置可以修改,否則代表系統的設計不佳。 盡一切努力避免程式碼重複(code duplication) ## YAGNI(You Ain't Gonna Need It) 不要為了未來的需求建立複雜的解決方案 針對現有需求編寫易於擴充的程式碼即可。 ## KISS(Keep It Simple, Stupid) 避免過度設計,已能解決問題的最小方案為主,不要用複雜的語法,盡量做到連新手都能看懂 ## LBYL(Look Before You Leap) 使用之前先檢查將使用的內容 例如開啟檔案前,檢查他是否可用 ## EAFP(Easier to Ask Forgiveness thane Permission) 先執行,如果失敗再來處理例外(不要浪費效能做事先檢查) ## python 中的繼承 繼承的子類別會對父類別產生耦合所以盡量避免 但是當元件定義一組介面而需要透過子類別進行介面行為的專門化時,是使用繼承的一個好時機 # SOLID 原則 ## 單一職責原則 Single Responsibility Principle 軟體元件(通常為類別) 必須僅承擔一個責任,只負責一件具體的事情,因此類別若要修改,必須只有一個理由。 類別越小越好,這樣可以產出有內聚力的軟體元件。 如果類別中有互斥或不相關的方法,表示需要進行拆分。 以下這個類別就是不好的設計 ``` python class SystemMonitor def load_activity(): ... def identify_events(): ... def stream_events(): ... ``` 改善:將方法拆成不同的class,每個類別只有一個職責 ```python class AlertSystem: def run(): ... class ActivityWatcher(AlertSystem): def load()->Events: ... class SystemMonitor(AlertSystem): def identify(Event): ... class Output(AlertSystem): def stream(List[Event]): ... ``` ## 開放封閉原則 Open Closed Principle 對擴展開放,對修改封閉 利用介面達到多型,在需要擴展時只需增加新的函式或是子類別,不用改到原本的程式。 ## 里氏替換原則 Liskov Substitution principle 對於任何類別來說,用戶端要能夠無差別地使用它的"子類別",在執行中不會損害預期行為,這表示用度端被隔離了,不知道類別階層的更改。 若S是T的子類別,應該要能把T替換成S而不會破壞程式的執行。 使用pylint 與 mypy 可以檢測里氏替換原則 ## 介面隔離原則 Interface Segregation Principle 介面應該很小 介面應該只做一件事(但不一定只有一個function) bad: ``` class EventParser: def from_xml(): def from_json(): ``` good: ``` class XMLEventParser(abc): class JSONEventParser(abc): class EventParser(XMLEventParser, JSONEventParser): ``` ## 依賴反轉原則 Dependency Inversion Principle 抽象被組織的方式不該依賴於細節,細節(具體實作)應該依賴抽象 透過提供介面去限制外部程式與我的程式接觸方式 (依賴注入屬於這個原則) ```python class EventStreamer: # 這邊的target就是一個依賴注入,可以注入任何符合DataTargetClient 介面的物件 def __init__(self, target: DataTargetClient): self._target = target def stream(self, events: list[Event]) -> None: for event in events: self._target.send(event.serialise()) ``` # 裝飾器 ## 帶參數的裝飾器 1. 函數裝飾器 ```python def decorator(*args, **kwargs): print("Inside decorator") def inner(func): # code functionality here print("Inside inner function") print("I like", kwargs['like']) func() # returning inner function return inner @decorator(like = "geeksforgeeks") def my_func(): print("Inside actual function") """ Inside decorator Inside inner function I like geeksforgeeks Inside actual function """ ``` 2. 類別裝飾器 ```python class CustomLogger(object): def __init__(self, in_prefix="<-", out_prefix="->"): self.in_prefix = in_prefix self.out_prefix = out_prefix def __call__(self, func): def wrapper(*args, **kwargs): print(self.in_prefix) func(*args, **kwargs) print(self.out_prefix) return wrapper docstr = CustomLogger @docstr() def functest(): print('In the functest') return """RESULT <- In the functest -> """ @docstr(in_prefix="^^^^^^", out_prefix="######") def functest_2(): print('In the functest') return functest_2() """RESULT ^^^^^^ In the functest ###### """ ``` + @wraps 的功用 讓函數的 __name__ 以及 __repr__ 可以正確顯示名稱及docstring