--- title: 'Python 物件導向、例外處理' disqus: kyleAlien --- Python 物件導向、例外處理 === ## Overview of Content 如有引用參考請詳註出處,感謝 :cat: [TOC] ## 類別、物件 Python 其實也是物件導向(`Object Oriented PRogramming`)範式的語言,而其中「類別」就像是封裝過後數據的模板,「物件」則是模板的實例(實體) ### 建立類別 Class * **建立類別**:Python 在建立類時與 Java 相同,都是使用 `class` 關鍵字… 語法格式如下 ```python= class 類別名稱(): # 括號 `()` 可以忽略 class 類別名稱: ``` Python 建立類範例如下 ```python= class Fruit: pass ``` ### 類別的屬性、方法 * **類別的屬性、方法**:類別中通常會建立屬性(`Attribute`)、方法(`method`)提供物件使用 * **Python 屬性** ```python= class Fruit: color: str = "Non" ``` * **Python 方法** Python 有個很不同的地方,一般的物件導向語言會在自身的類內部隱性建立一個 `this` 屬性用來代表物件自身,而 **Python 則「顯性」的展示出物件自身 `self`** 所以在定義方法時,第一個參數一定是 `self`,在類別內部可以 `self` 取得物件自身的數據;但是在呼叫方法時不需要設定 `self` ```python= class Fruit: color: str = "Apple" # 一定要有 self 參數 def printName(self): print("Fruit, color: " + self.color) ``` 使用類範例如下: ```python= if __name__ == '__main__': fruit = Fruit() print(f"color: {fruit.color}") fruit.printName() # 在呼叫方法時不需要設定 `self` ``` > ![image](https://hackmd.io/_uploads/Syedb0ToJC.png) ### 類別的建構式 * 建立類別時如果需要設定初始化參數(就像是規定建立類之前所需要的數據),就可以透過「建構式」;**在 Python 中,建構式有個固定的名稱為 `__init()__`**,其格式如下 ```python= def __init(self[, arg1, arg2, ...]) ``` :::info * 建構式同樣要使用 `self` 參數 * 如果一個類沒有指定的數據就可以建構,那就不需要特別寫建構式,因為 Python 會隱含建立一個 `__init__(self)` 建構式 ::: 建構式的使用範例如下 ```python= class Fruit: def __init__(self, color: str): # 指定要傳入 color 參數 # 這裡會建構一個 color attribute self.color = color def printName(self): print("Fruit, color: " + self.color) ``` 使用類、建構式範例如下: ```python= if __name__ == '__main__': fruit = Fruit("red") # 傳入 color 參數 print(f"color: {fruit.color}") fruit.printName() ``` > ![image](https://hackmd.io/_uploads/r1x4z0iJ0.png) ## Python 物件導向:標準特性 我們都知道物件導向有三大特性「封裝」、「繼承」、「多型」,而 Python 同樣有這三個特點,不過它的表現方式(語法)稍加不同,這裡我們就要來認識 Python 實現物件導向的語法 ### 封裝 encapsulation * 封裝(`encapsulation`)的特性可以把物件所需的資料關在類(`Class`)之中,並對外隱藏容易改變的數據,只公開要提供的穩定服務! * **Python 隱藏(私有)數據**: 一般來講隱藏物件中的屬性或是方法需要透過 `private` 關鍵字(像是 Java)描述,而 Python 則沒有提供 `private` 關鍵字,而是透過 `__`(雙底線)來描述要隱藏的屬性或是方法,範例如下 ```python= class Fruit: def __init__(self, color: str): self.__color = color def __prize(self) -> int: return 100 def printName(self): print("Fruit, color: " + self.__color) ``` :::waring * 由於 Python 屬於動態語言,所以不會強制檢查,但是相對的,如果使用私有數據、方法它則會透過提醒 `Unresolved attribute reference` 來警告 > ![image](https://hackmd.io/_uploads/HJylL0jkC.png) 運行時就會拋出 `AttributeError` 異常 > ![image](https://hackmd.io/_uploads/SyL9v0s1R.png) ::: 測試封裝範例如下: ```python= if __name__ == '__main__': fruit = Fruit("red") fruit.printName() ``` > ![image](https://hackmd.io/_uploads/SkxWPCjJR.png) ### 繼承 Inheritance * 在 Python 中,所有的類別都是可以被繼承的,被繼承的類稱之為父類別(`Parent class`, 或是稱為基類 `base class`);繼承的類別則稱之為子類別(`Child class`, 或是衍生類 `derived class`) Python 建立子類別的格式如下 ```python= class 子類別(父類別): ``` 創建父子類別的範例如下 ```python= class Fruit: # 父類 pass class Apple(Fruit): #子類 pass ``` * **OOP 中,父、子類別的資料關係**: 子類別會繼承父類別的所有屬性、方法(**包括私有方法,但是父類別的私有方法子類別無法使用**),而子類別也可以擁有自己的方法,概念圖如下… ```mermaid graph LR subgraph 子類別 subgraph 父類別 屬性 方法 end 子類別自有的方法 end ``` 父、子類別的資料關係範例如下 ```python= class Fruit: # 父類 def show_information(self): print(f"This is fruit") class Apple(Fruit): # 子類 def __init__(self, color): self.color = color # 自身的資料 def show_apple_info(self): print(f"Apple, color({self.color})") if __name__ == '__main__': apple = Apple("Green") apple.show_apple_info() # 呼叫自身的方法 apple.show_information() # 呼叫父類別的方法 ``` > ![image](https://hackmd.io/_uploads/HyyK2RjJR.png) * 如果要呼叫父類別的方法可以透過 `super` 關鍵字 + 方法明呼叫,範例如下 ```python= class Fruit: # 父類 def __init__(self, prize): # 父類別創建建構函數 self.prize = prize def show_information(self): print(f"This is fruit, prize({self.prize})") class Apple(Fruit): # 子類 def __init__(self, color): super().__init__(100) # 子類別需要透過 `super` + 方法名來呼叫父類別的方法 self.color = color def show_apple_info(self): print(f"Apple, color({self.color})") if __name__ == '__main__': apple = Apple("Green") apple.show_apple_info() apple.show_information() ``` > ![image](https://hackmd.io/_uploads/r1j56Ao1R.png) ### 多型 Polymorphism * 在程式中,多型的意思簡單來說是「擁有相同的方法名稱但是有不同的行為」,透過抽象來達成不同的行為 * 其中多型有分為「**靜態多型**」、「**動態多型**」,其兩者個差異在於 **多型形成的時間** * **靜態多型是在編譯期間就決定的多型**,效能較好但是可變性有限,靜態多型範例如下(透過 `override` 函數達成) ```python= class Fruit: # 父類 def __init__(self, prize): self.prize = prize def show_information(self): print(f"This is fruit, prize({self.prize})") class Apple(Fruit): # 子類 def __init__(self, color): super().__init__(100) self.color = color # 靜態多型 def show_information(self): print(f"This is apple, color({self.color}, prize({self.prize})") if __name__ == "__main__": Fruit(300).show_information() Apple("Red").show_information() ``` > ![image](https://hackmd.io/_uploads/BJ_UJNTk0.png) * **動態則是在運行期間才決定多型的型態**,效能較差但是彈性極高,動態多型範例如下 ```python= def show_info(fruit: Fruit): # 動態多型,在運行期間才決定連結 fruit.show_information() if __name__ == "__main__": show_info(Fruit(300)) show_info(Apple("Red")) ``` > ![image](https://hackmd.io/_uploads/BJ_UJNTk0.png) ## Python 物件導向:特殊特性 Python 除了上面我們看到的標準的物件導向特性之外,它自身也有非標準(特殊)的物件導向特性 ### 動態類型語言:鴨子類型 * **Python 鴨子類型**:鴨子類型的特性很常體現在動態語言中 > 鴨子類型只是一種隱喻,其實就是說沒有任何關係的類,可以在運行期間產生關係 Python 的鴨子類型也是一種動態語言的體現,它沒有靜態語言的垂直繼承樹的限制,也就是說… Python 中可以沒有繼承關係而達到多型,範例如下 ```python= # 以下的 Swan、Duck 並沒有任何關係 class Swan: def shout(self): print("Swan shout") class Duck: def shout(self): print("Duck shout") def shout(object): object.shout() # 但在運行時可以有多型的特性 if __name__ == "__main__": shout(Swan()) shout(Duck()) ``` > ![image](https://hackmd.io/_uploads/BkBOMVayA.png) ### 多重繼承 * 多重繼承在其他語言上(包括靜態語言)也同樣有實現,像是 Dart、C++... 等等語言 :::warning 然而多重繼承在開發上並不是每個語言都推崇,因為多重繼承會增加 **程式的複雜度**,包括其可見、可理解性都依賴繼承順序上的細節 > 甚至可能產生菱形狀的繼承,那也不利於可讀性 ::: * Python 的多重繼承格式如下 ```python= class 子類別(父類別1, 父類別2, 父類別3... 父類別n): ``` Python 多重繼承的範例如下 ```python= class WorkAction: def work(self): print("I am working now.") class ShopAction: def shopping(self): print("I am shopping now.") class Person(WorkAction, ShopAction): def daily(self): super().work() super().shopping() if __name__ == '__main__': Person().daily() ``` > ![image](https://hackmd.io/_uploads/ryYLUET10.png) :::danger * **多重繼承的危險性**: 多重繼承要特別注意的就規則就是 **==繼承順序==**,**繼承順序會影響到搜尋的實做類**… 搜尋的順序是子類別自身,接著是「**同一階層父類別,由左至右**」 ```python= class WorkInfo: def info(self): print("coding now.") class ShopInfo: def info(self): print("But fruit.") ``` 接著,觀察同階層父類別繼承順序導致的影響 1. `WorkInfo` 至於左邊:可以看到它被提前搜尋到,所以會是 `WorkInfo` 的實做 ```python= class PersonInfo(WorkInfo, ShopInfo): pass ``` > ![image](https://hackmd.io/_uploads/rJ4Bv4TyC.png) 2. `ShopInfo` 至於左邊:同上,但由於是 `ShopInfo` 先被搜尋到,所以實做會是 `ShopInfo` ```python= class PersonInfo(ShopInfo, WorkInfo): pass ``` > ![image](https://hackmd.io/_uploads/B1L3PEpkR.png) ::: ## 例外處理 Python 如同 Java/Kotlin 一樣,都可以捕捉運行時的異常,在程式設計中可以依照類的設計角色、階層作到例外轉譯的功能 ### try-except-else-fainlly 語法、使用 * Python 捕捉異常的關鍵語句是 `try-except-else-fainlly`,透過它就可以捕捉異常,其特性如下 * **`except` 特性**: 1. 語句中必須要有 `1..*` 的 `except` 2. 一個 `except` 語句一次可以捕捉多種不同異常 3. `except` 如果沒有指定例外的話,則表示捕捉所有的例外(也包括系統例外) 4. 捕捉的 `except` 例外判斷是由上至下,範為由小到大(Exception 如果是小的話,BaseException 就是大) 5. 捕捉例外後不一定要有參數,如果想要有參數使用,則須透過 `as` 關鍵字創建參數 * **`else` 特性**: `else` 的特性是「如果例外沒有發生才會被執行」,如果用別的語言表達 `else` 概念如下(用 Java 語言表達) :::warning 這是個很好用的語法糖,**可以幫助我們專注處理非異常的裝況** ::: ```java= void tryCatchWithElse(Runnable runablle) { boolean isOccurException = true; try { 可能發生例外的程式; isOccurException = false; } catch(Exception e) { 處理裡外 } finally { if (!isOccuException) { // 沒有發生異常,才會執行 runablle.run(); } 執行最後的程式 } } ``` * **Python 使用 `try-except-else-fainlly` 語句的使用結構如下**… ```python= try: 可能發生例外的程式 except 例外類A [as 參數]: 處理例外A except (例外類B, 例外類C) [as 參數]: # 可以一次捕捉多個異常 處理例外B、C except: 除去 ABC 例外的所有例外 else: 程式正確執行時才會執行這裡 finally: 不管成功、失敗都會執行這裡 ``` 使用範例如下 * **使用 `try-exception-else` 語句** ```python= def try_except_else(): try: print(1 / 0) except: print("Occurs exception") else: print("No exception") ``` > ![image](https://hackmd.io/_uploads/ByKEL6ul0.png) * **使用 `try-exception-else` 語句配合 `as` 關鍵字** ```python= def try_except_as_else(): try: print(1 / 0) except Exception as e: print(f"Occurs exception, {e}") else: print("No exception") ``` > ![image](https://hackmd.io/_uploads/B1t6Iade0.png) * **使用 `try-exception-else-fainlly` 語句配合 `as` 關鍵字** * 運行在「發生異常」的程式之下 ```python= def try_except_as_else_finally(): try: print(1 / 0) except Exception as e: print(f"Occurs exception, {e}") else: print("No exception") finally: print("Finally action") ``` 可以看到,就算發生異常 `finally` 也的確會被執行 > ![image](https://hackmd.io/_uploads/ByKVwa_xA.png) * 運行在「沒有異常」的程式之下 ```python= def try_except_as_else_finally(): try: print(1 / 0) except Exception as e: print(f"Occurs exception, {e}") else: print("No exception") finally: print("Finally action") ``` 從下圖的執行結果中,我們可以觀察到,**`else` 會比 `finally` 還要早執行** > ![image](https://hackmd.io/_uploads/Hk1zuTux0.png) ### Python 常見的異常 * **Python 常見的異常我們分為幾個,如下表所示** * **內建異常** | 異常類型 | 概述 | | - | - | | SyntaxError | Python 代碼的語法錯誤 | | IndentationError | 縮排錯誤 | | TabError | 使用了不一致的縮進 | | NameError | 使用了未定義的變量名或函數名 | | TypeError | 類型不匹配 | | ValueError | 類型正確但值不合法 | | KeyError | 字典中查找不存在的鍵 | | IndexError | 列表或字符串索引越界 | * **文件操作異常** | 異常類型 | 概述 | | - | - | | FileNotFoundError | 試圖打開不存在的文件 | | IOError | 文件 IO 錯誤 | | PermissionError | 權限不足 | * **網路相關異常** | 異常類型 | 概述 | | - | - | | ConnectionError | 連接錯誤,是以下異常的基類 | | TimeoutError | 連接超時 | | HTTPError | HTTP 錯誤 | | URLError | URL 錯誤 | * **其他異常** | 異常類型 | 概述 | | - | - | | KeyboardInterrupt | 用戶中斷執行(例如按下 Ctrl+C) | | SystemExit | 系統退出 | | OverflowError | 堆溢出 | | ArithmeticError | 數學計算錯誤 | | ZeroDivisionError | 除零錯誤 | | MemoryError | 記憶體錯誤 | | ImportError | 導入模塊錯誤 | | ModuleNotFoundError | 導入不存在的模塊 | | RuntimeError | 運行時錯誤 | | AssertionError | 斷言語句失敗 | | DeprecationWarning | 過時警告 | ### raise 拋出異常、自訂異常 * 前面我們有說到 [「**例外轉譯**」](https://devtechascendancy.com/java-jvm-exception-handling-guide/#%E4%BE%8B%E5%A4%96%E8%BD%89%E8%AD%AF_%E4%BE%8B%E5%A4%96%E9%8F%88),它的概念就是,在補捉到異常時依照當前模組的層級、角色來拋出更加符合表達狀況的異常(這對於程式設計來說很重要) * Python 如果要拋出異常的話需 **使用 `raise` 關鍵字**,語法如下 ```shell= raise [例外類型] ``` 使用範例如下 ```python= def raise_except(value): if value is not int: raise TypeError("Not an int") elif value == 0: raise ValueError("illegal value") print(f"{1 / value}") if __name__ == '__main__': raise_except(0) ``` > ![image](https://hackmd.io/_uploads/SyfJfRde0.png) * 除此之外,如果 Python 沒有提供我們所需要的異常,我們也可以自定義異常;在自訂異常時僅僅需要定義一個類,並讓該類繼承於 `Exception` 類 自定義異常範例 ```python= # 自訂異常 class HandleException(Exception): def __init__(self, message): self.message = message # 例外轉譯 def use_handle_exception(): try: print(1 / 0) except: raise HandleException("raise exception") if __name__ == '__main__': # 捕捉自定義異常 try: use_handle_exception() except HandleException as e: print(e.message) ``` ### 異常紀錄到檔案:traceback 模組 * 如果有需要的話,我們可以透過 `traceback` 模組的 `format_exce()` 函數,將異常紀錄到外部的檔案中; ```python= import traceback ``` 使用 `traceback` 範例如下 ```python= import traceback def use_traceback(): exception: Exception try: print(1 / 0) except Exception as e: exception = e print(f"Occurs exception, {e}") else: print("No exception") finally: if exception: print("record exception") traceback.format_exc() print("Finally action") ``` > ![image](https://hackmd.io/_uploads/rkizuRdl0.png) ### assert 斷言 * **`assert` 主要用途是在「程式開發的階段」** 做為開發提供軟體服務的一方,可以透過 `assert` 關鍵字來驗證使用者是否有依照你的設計規範來應用,也幫助我們在開發階段就發現問題 `assert` 的語法如下 ```python= assert 條件式, 參數 ``` * 「斷言」的含意其實就是,保證條件式一定如我們所想,如果非我們所想,則會拋出 `AssertErro` 異常;使用範例如下 ```python= def use_assert(value): assert isinstance(value, int), f"{value} is not an int" assert value != 0, f"{value} is zero" print(1 / value) if __name__ == '__main__': use_assert("0") ``` > ![image](https://hackmd.io/_uploads/B1WgyktlA.png) :::info * 如果運行時想忽略斷言,可以使用 `-O` 選項 ```shell= python -O xxx.py ``` ::: ## Appendix & FAQ :::info ::: ###### tags: `Python`