# Lesson 5 | 物件導向導論
## 第一節:熟悉創建物件(1)
* 不知道同學們有沒有注意到目前為止除了函式以外,大部分的變數在輸入```type()```後,都會出現class的字眼。
```python=
list_a = [1,2,3]
print(type(list_a))
```
* 「<class 'list'>」代表的是該變數是一個類別(class)為list的物件。
* 這堂課將要來定義我們自己的類別,並創建其物件。
## 第一節:熟悉創建物件(2)
* 下列程式碼是定義類別的範例。
```python=
class MyComplex():
def __init__(self, real_part, imag_part):
# self.r 與 self.i 是這個物件的「屬性」(data attribute),
# 或稱為instance variables。
self.r = real_part
self.i = imag_part
```
* 像函式一樣,我們可以透過小括號來「實例化」我們的類別。
* 創建時需輸入```__init__```該有的參數。
```python=
Complex_1 = MyComplex()
```
```python=
Complex_1 = MyComplex(1, 2)
# 這裡可以看到Complex_1的類別是我們定義的MyComplex
print(type(Complex_1))
print(Complex_1.r)
print(Complex_1.i)
# 我們也可以透過以下程式碼來修改物件的instance variables。
Complex_1.r = 3
print(Complex_1.r)
Complex_1.i = 6
print(Complex_1.i)
```
* 在這裡我們重新定義關於這個類別的一個「方法」(method),方法就是「a function that “belongs to” an object」。
```python=
class MyComplex():
def __init__(self, real_part, imag_part):
self.r = real_part
self.i = imag_part
def print_real_imag(self):
print("The real part of this object: ", self.r)
print("The imaginary part of this object: ", self.i)
Complex_1 = MyComplex(1, 2)
Complex_1.print_real_imag()
```
## 第一節:熟悉創建物件(3)
* 同學們應該可以想像到,跟函式一樣的邏輯,物件導向的設計可以縮減許多重複的部分,並且利於共同開發。
* 還記得第一課的BMI指數計算器嗎?計算n個人的BMI程式碼可能會長的像這樣。
```python=
height = 163
weight = 60
BMI = (weight / ((height/100)**2))
print("你輸入的BMI為", round(BMI, 2), " (kg/m^2).")
if BMI < 18.5:
result = '體重過輕'
elif BMI < 24:
result = '健康體重'
elif BMI < 27:
result = '體重過重'
else:
result = '肥胖'
print("體重狀況: ", result)
height = 172
weight = 65
BMI = (weight / ((height/100)**2))
print("你輸入的BMI為", round(BMI, 2), " (kg/m^2).")
if BMI < 18.5:
result = '體重過輕'
elif BMI < 24:
result = '健康體重'
elif BMI < 27:
result = '體重過重'
else:
result = '肥胖'
print("體重狀況: ", result)
```
* 在這裡,我們將定義新的一個類別:「BMICalculator」。
```python=
class BMICalculator():
def __init__(self, height, weight):
self.set_height_weight(height, weight)
def set_height_weight(self, height, weight):
self.height = height
self.weight = weight
def print_the_BMI_status(self):
BMI = (self.weight / ((self.height/100)**2))
print("BMI為", round(BMI, 2), " (kg/m^2).")
if BMI < 18.5:
result = '體重過輕'
elif BMI < 24:
result = '健康體重'
elif BMI < 27:
result = '體重過重'
else:
result = '肥胖'
print("體重狀況: ", result)
```
* 實際運用的時候會簡潔非常多。
```python=
My_BMICalculator = BMICalculator(163, 70)
My_BMICalculator.print_the_BMI_status()
```
```python=
My_BMICalculator.set_height_weight(173, 60)
My_BMICalculator.print_the_BMI_status()
```
```python=
My_BMICalculator.set_height_weight(184, 58)
My_BMICalculator.print_the_BMI_status()
```
* 當程式碼非常複雜的時候,我們可以更結構化的包裝程式碼,而且重點在於,物件的「屬性」可以重複利用,也就是不需再重複計算。另外,也可以避免變數名稱的汙染,也就是物件導向中所謂的「封裝」。
## 練習1:
* 現在我將與同學們共同開發這個BMI指數計算器,目前BMI指數計算器已經完成了大部份,還有以下功能想做到。
* 問題1:請同學們幫我新增一個方法```print_height_weight()```,讓這個類別能夠顯示目前的身高與體重(請用```print()```)。
* 問題2:請同學們再幫我修改```set_height_weight()```這方法,當輸入的```height```小於20"或"大於250時顯示警告,且當```weight```小於0"或"大於200時顯示警告。
* 完成後可以用下列程式碼測試。
```python=
My_BMICalculator = BMICalculator(-5, 10)
My_BMICalculator.print_height_weight()
My_BMICalculator.print_the_BMI_status()
print()
My_BMICalculator.set_height_weight(165, 60)
My_BMICalculator.print_height_weight()
My_BMICalculator.print_the_BMI_status()
print()
My_BMICalculator.set_height_weight(188, 65)
My_BMICalculator.print_height_weight()
My_BMICalculator.print_the_BMI_status()
```
<details>
<summary>解答</summary>
```python=
class BMICalculator():
def __init__(self, height, weight):
self.set_height_weight(height, weight)
def set_height_weight(self, height, weight):
if height < 20 or height > 250:
print("身高請輸入介於20與250之間")
self.height = height
if weight < 0 or weight > 200:
print("體重請輸入介於0與200之間")
self.weight = weight
def print_height_weight(self):
print('身高:', self.height, '體重:', self.weight)
def print_the_BMI_status(self):
BMI = (self.weight / ((self.height/100)**2))
print("BMI為", round(BMI, 2), " (kg/m^2).")
if BMI < 18.5:
result = '體重過輕'
elif BMI < 24:
result = '健康體重'
elif BMI < 27:
result = '體重過重'
else:
result = '肥胖'
print("體重狀況: ", result)
```
* 同學們有沒有發現我們只需對部分程式碼進行修改,這就是物件導向共同撰寫的優勢之一。
</details>
## 第二節:繼承(1)
* 「繼承」也是物件導向中的一個重要的特性,就字面上來說,我們可以讓A類別延伸出B類別,B類別就可帶有相同的屬性設定與功能。
* 現在這邊有兩個類別。
* 一個是用來儲存病人基本人口學的類別。
```python=
class PatientDemo():
def __init__(self, cno):
self.cno = cno
def set_age(self, age):
self.age = age
def print_cno(self):
print("病人的病歷號為", self.cno)
def print_demo(self):
print("病人的年齡為", self.age)
```
```python=
Demo_A = PatientDemo(cno = 1509745)
Demo_A.set_age(60)
Demo_A.print_cno()
Demo_A.print_demo()
```
* 另一個是用來儲存病人生化檢測結果的類別。
```python=
class PatientLab():
def __init__(self, cno):
self.cno = cno
def set_serum_potassium(self, k):
self.k = k
def print_cno(self):
print("病人的病歷號為", self.cno)
def print_lab_results(self):
print("病人的血鉀濃度為", self.k)
```
```python=
Lab_A = PatientLab(cno = 1509745)
Lab_A.set_serum_potassium(4.5)
Lab_A.print_cno()
Lab_A.print_lab_results()
```
## 第二節:繼承(2)
* 基於「繼承」的撰寫方法,我們可以考慮建構一個更高階的類別```PatientData```(病人資料)。
* 透過繼承,讓生化檢測結果和基本人口學去繼承病人資料,可使基本人口學```DemoData```與生化檢測```LabData```和都具備病人資料的內涵。
```python=
class PatientData():
def __init__(self, cno):
self.cno = cno
def print_cno(self):
print("病人的病歷號為", self.cno)
class DemoData(PatientData):
def set_age(self, age):
self.age = age
def print_demo(self):
print("病人的年齡為", self.age)
class LabData(PatientData):
def set_serum_potassium(self, k):
self.k = k
def print_lab_results(self):
print("病人的血鉀濃度為", self.k)
```
* 用起來像這樣。
```python=
Demo_A = DemoData(cno = 1509745)
Demo_A.print_cno()
Demo_A.set_age(50)
Demo_A.print_demo()
```
```python=
Lab_A = PatientLab(cno = 1509745)
Lab_A.set_serum_potassium(4.5)
Lab_A.print_cno()
Lab_A.print_lab_results()
```
* 這樣的好處在於,若我們要新增匹如說超音波資料```EchoData```,就可以在不需要更動基本人口學```DemoData```與生化檢測```LabData```的情況下,延伸創建一個具有病人資料```PatientData```特性的新類別,這樣的繼承關係,也可稱為父類別與子類別。
## 練習2:
* 問題: 請同學幫我創建一個超音波資料```EchoData```的類別,請繼承```PatientData```。
* 第一,請定義一個名稱為```set_ejection_fraction```的方法將讓```EchoData```可以儲存心臟射出率(ejection fraction),讓```ef```為```EchoData```的instance variables。
* 第二,請參考以下函式,讓```EchoData```可以依照病人的ejection fraction,回傳病人是否為left ventricular dysfunction(ejection fraction小於等於40)。
```python=
def print_lvd(ef):
if ef <= 40:
print("病人可能具有左心室功能障礙")
else:
print("病人的心臟射出率為正常")
```
* 完成後可以用下列程式碼測試。
```python=
Echo_A = EchoData(cno = 1509745)
Echo_A.set_ejection_fraction(30)
Echo_A.print_cno()
Echo_A.print_lvd()
```
<details>
<summary>解答</summary>
```python=
class EchoData(PatientData):
def set_ejection_fraction(self, ef):
self.ef = ef
def print_lvd(self):
if self.ef < 40:
print("病人可能具有左心室功能障礙")
else:
print("病人的心臟射出率為正常")
```
</details>
## 第三節:套件化程式碼(1)
* 在先前的課程中已經教大家如何載入標準函式庫以及安裝與載入第三方套件,除此之外,現在應該已經會撰寫自定義的類別了,接下來要教大家如何在本機(Local)將自己所撰寫的類別套件化。
* 先請在當前的工作目錄下,新建立一個叫```my_package```的資料夾,並在裡面放入兩個檔案。
* 第一個檔案的檔名為```__init__.py```,內容為空。
```python=
```
* 第二個檔案的檔名為```ModuleA.py```,內容為我們剛剛所撰寫的類別:
```python=
class PatientData():
def __init__(self, cno):
self.cno = cno
def print_cno(self):
print("病人的病歷號為", self.cno)
class DemoData(PatientData):
def set_age(self, age):
self.age = age
def print_demo(self):
print("病人的年齡為", self.age)
class LabData(PatientData):
def set_serum_potassium(self, k):
self.k = k
def print_lab_results(self):
print("病人的血鉀濃度為", self.k)
class EchoData(PatientData):
def set_ejection_fraction(self, ef):
self.ef = ef
def print_lvd(self):
if self.ef < 40:
print("病人可能具有左心室功能障礙")
else:
print("病人的心臟射出率為正常")
```
* 這樣子就完成了我們的初步部屬囉。
## 第三節:套件化程式碼(2)
* 設定完後載入套件其實不難,重點在於```import```套件時的工作位置以及資料夾相對關係。
* 讓我們回到原本的工作目錄下,執行下列程式碼:
```python=
from my_package.ModuleA import *
print(PatientData(5555))
# 顯示 <my_package.ModuleA.PatientData object at 0x7f77a8792fa0>
# 代表我們實例化了的my_package套件底下的ModuleA的類別PatientData
# 想當然爾也可以使用ModuleA裡自定義的類別EchoData
Echo_A = EchoData(cno = 1509745)
Echo_A.set_ejection_fraction(30)
Echo_A.print_cno()
Echo_A.print_lvd()
```
```python=
# 我們也可以只載入特定類別,這樣更能看清楚我們載入了甚麼類別
from my_package.ModuleA import EchoData
Echo_A = EchoData(cno = 1509745)
Echo_A.set_ejection_fraction(30)
Echo_A.print_cno()
Echo_A.print_lvd()
```
## 第三節:套件化程式碼(3)
* 除此之外,當專案越來越大時,套件底下的模組也是可以有多個層級的,如下圖範例。

## 第三節:套件化程式碼(4)
* 目前工作目錄下的my_package套件裡,已經有儲存與顯示病人資料的一些基本模組了。
* 而其實我們也可以透過像這樣載入套件的方式,載入一般的函式,請同學們在my_package底下新增```ModuleB.py```,並在```ModuleB.py```複製貼上下列程式碼。
* 這是一個簡單的csv讀檔函式,我們在第三堂課以及第四堂課都有介紹相關的內容。
```python=
import csv
def csv_loader(file_path):
with open(file_path, 'r', encoding = 'CP950') as f:
reader = csv.DictReader(f)
headers = reader.fieldnames
data = {}
for i in headers:
data[i] = []
for row in reader:
for k, v in row.items():
data[k].append(v)
return data
```
* 建立完```ModuleB.py```後,讓我們回到原本的工作目錄下,並執行下列程式碼:
```python=
from my_package.ModuleB import csv_loader
print(csv_loader)
# 顯示<function csv_loader at 0x7fa4443b48b0>
```
## 練習3:
* 請同學們先練習著使用這些模組,請先下載這個[檔案](https://drive.google.com/file/d/1zMJ2TnENQQm7vLhaRSwPVEo0Y1N7mR40/view?usp=sharing),這是一個有病人病歷號以及左心室射出率的簡單模擬檔案。
* 問題:請利用程式告訴我們哪些病人有左心室功能障礙,請依照下列流程撰寫程式碼:
* 請先載入```ModuleA```的```EchoData```以及載入```ModuleB```的```csv_loader```,接著,利用```csv_loader```載入檔案後,使用```EchoData```以及其```print_lvd()```方法。
* 你可能需要撰寫一個簡單的迴圈,在讀進檔案後,將每一筆資料拋給```EchoData```以及其方法。
```python=
for i in range(len(echo_data['ID'])):
sub_id = echo_data['ID'][i]
sub_ej = echo_data['ejection_fraction'][i]
print(sub_id)
print(sub_ej)
```
<details>
<summary>解答</summary>
```python=
from my_package.ModuleA import EchoData
from my_package.ModuleB import csv_loader
echo_data = csv_loader('echo.csv')
for i in range(len(echo_data['ID'])):
sub_id = echo_data['ID'][i]
sub_ej = echo_data['ejection_fraction'][i]
Echo_A = EchoData(cno = sub_id)
Echo_A.set_ejection_fraction(sub_ej)
Echo_A.print_cno()
Echo_A.print_lvd()
print()
```
</details>
## 總結
* 本次課程介紹了基礎的物件導向撰寫概念,同學們也有能力寫出自定義的類別,並在不同的檔案中撰寫程式碼。
* 雖然同學們需要大量的練習才能更夠熟悉物件導向式的撰寫模式,但透過本堂課的介紹,同學們在使用第三方套件的類別時,應能更了解該類別可能的運作方式,在接下來的課程裡,將會帶領各位同學使用不同類型的第三方套件,讓同學們有能力完成各項任務。