---
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`
```
> 
### 類別的建構式
* 建立類別時如果需要設定初始化參數(就像是規定建立類之前所需要的數據),就可以透過「建構式」;**在 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()
```
> 
## 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` 來警告
> 
運行時就會拋出 `AttributeError` 異常
> 
:::
測試封裝範例如下:
```python=
if __name__ == '__main__':
fruit = Fruit("red")
fruit.printName()
```
> 
### 繼承 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() # 呼叫父類別的方法
```
> 
* 如果要呼叫父類別的方法可以透過 `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()
```
> 
### 多型 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()
```
> 
* **動態則是在運行期間才決定多型的型態**,效能較差但是彈性極高,動態多型範例如下
```python=
def show_info(fruit: Fruit): # 動態多型,在運行期間才決定連結
fruit.show_information()
if __name__ == "__main__":
show_info(Fruit(300))
show_info(Apple("Red"))
```
> 
## 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())
```
> 
### 多重繼承
* 多重繼承在其他語言上(包括靜態語言)也同樣有實現,像是 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()
```
> 
:::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
```
> 
2. `ShopInfo` 至於左邊:同上,但由於是 `ShopInfo` 先被搜尋到,所以實做會是 `ShopInfo`
```python=
class PersonInfo(ShopInfo, WorkInfo):
pass
```
> 
:::
## 例外處理
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")
```
> 
* **使用 `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")
```
> 
* **使用 `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` 也的確會被執行
> 
* 運行在「沒有異常」的程式之下
```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` 還要早執行**
> 
### 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)
```
> 
* 除此之外,如果 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")
```
> 
### 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")
```
> 
:::info
* 如果運行時想忽略斷言,可以使用 `-O` 選項
```shell=
python -O xxx.py
```
:::
## Appendix & FAQ
:::info
:::
###### tags: `Python`