物件導向 (Object-oriented) & Class === ## What is object / class - **類別(Class):** 一個概念、模板 - **物件(Object):** 實際的物體 我們要設計一個 **實際的東西**(物件)會需要 **設計圖**(類別),設計圖中會定義該物體的 **屬性**(attribute, property, characteristic)還有其 **功能**(method, action) :::success 類別需要被 **實體化(initialization)** 才會變成物件 ::: #### 生活例子 我們要設計一隻狗勾(object)會需要設計圖(class)來明確列出怎麼做 ⇒ 因此 class 不是一隻真的狗,是要照著設計圖做出來(initialization)的東西才是真的狗 ***今天想做一隻吉娃娃*** - 吉娃娃會有一些基本屬性 - 短短的腿 - 大大的眼睛 - 吉娃娃會做的動作 - 瘋狂亂叫(woof! woof!) - 吐舌頭(:p) #### 程式例子 - 字串`str`是一個類別 - `greetSTR = "Hello World!"` 是一個變數`greetSTR`被賦予一個字串`"Hello World!"`當作其值 **這就是一個將字串這個類別實體化的動作** ⇒ ==`greetSTR`是一個類別為`str`的物件== ## Python 中的 Class 寫法 ### 1. 建立類別 ```python # 1 定義類別 class Dog: # 2 建構式 (constructor) -> 初始化函數 def __init__(self, name, leg, eye): # 具有 name, leg 以及 age 的屬性 self.name = name self.leg = leg self.eye = eye # 3 設定方法 def bark(self, sentence): # 具有 bark 的行為 return self.name + " says " + sentence def face(self, action): # 具有 face 的動作 return f"{self.name}({action})" ``` 1. 定義類別:類別可以看成是許多變數與函數的包裝器(為這個設計圖取個名) - 通常會用第一個字大寫,後面括號可加可不加(版本問題,新版可以不用加了) 2. 初始化函數(屬性設定):`def __ init __ (self)` 這個語法稱為**建構式(constructor)** - class底下的function第一個參數都會是 `self` (convention) - 後面可以加上自己想要定義的其他屬性`def __ init __ (self, *args)` 3. 設定方法:`def bark (self, *args)` - 依據類別及設計概念新增方法(功能函數),一樣第一個參數需要傳入 `self`,其餘參數以及實作細節依需求設定。 ### 2. 實例化 ```python chiwawa = Dog('Chiwawa', 'short', 'big' ) # 1 實體化類別 print(chiwawa.name, chiwawa.leg, chiwawa.eye) # 2 獲取屬性 print(chiwawa.bark("'woof! woof!'")) # 3 使用方法 print(chiwawa.face(":p")) # 3 使用方法 # ------ output ------ # Chiwawa short big # Chiwawa says 'woof! woof!' # Chiwawa(:p) ``` 1. 新增一個變數負責來實例化該類別形成物件 2. 創建後就可以使用 `.` 來獲取物件裡面的屬性 3. 使用 `.` 來使用物件裡面的方法 ## 物件導向三大特性 ### 1. 封裝 Encapsulation 將方法跟屬性包在一個類別底下來保護其內部的狀態不被外部直接訪問或修改。封裝的目的是隱藏物件的實現細節,只暴露一個公共介面,使得物件的使用者只需知道其提供的服務,而不必知道內部的實現細節。 > 我們去餐廳點餐,只需要知道廚師會送上什麼東西,不需要知道食物是怎麼被煮出來的。 #### 沒有封裝 ```python from datetime import datetime as dt # 基本資訊 name = 'Mary' age = 25 attendance = {'clock_in': [], 'clock_out': []} # 顯示員工資訊 def staff_info(name: str, age: int, attendance: dict): print(f"{name}: {age}\n----- Attendance -----") for state, times in attendance.items(): print(f"{state}: {times}") # 記錄打卡 def clock_in_and_out(name: str, state: str) -> str: time_now = dt.now().strftime('%Y-%m-%d %H:%M:%S') attendance[state].append(time_now) return f"{name}: {state.replace('_', ' ').capitalize()} at {time_now}!" # 未打卡 staff_info(name, age, attendance) # 打卡上班 print(clock_in_and_out(name, 'clock_in')) staff_info(name, age, attendance) # 打卡下班 print(clock_in_and_out(name, 'clock_out')) staff_info(name, age, attendance) # ------ output ------ # Mary: 25 # ----- Attendance ----- # clock_in: [] # clock_out: [] # Mary: Clock in at 2024-10-16 16:25:23! # Mary: 25 # ----- Attendance ----- # clock_in: ['2024-10-16 16:25:23'] # clock_out: [] # Mary: Clock out at 2024-10-16 16:25:23! # Mary: 25 # ----- Attendance ----- # clock_in: ['2024-10-16 16:25:23'] # clock_out: ['2024-10-16 16:25:23'] ``` #### 有封裝 ```python from datetime import datetime as dt class Worker: def __init__(self, name: str, age: int, ID: str): self.name = name # 公有屬性 self._age = age # 受保護的屬性 self.__ID = ID # 私有屬性 self.attendance = {'clock_in': [], 'clock_out': []} def staff_info(self) -> dict: print(f"{self.name}: {self._age}\n----- Attendance -----") for state, times in self.attendance.items(): print(f"{state}: {times}") return self.attendance def clock_in_and_out(self, state: str) -> str: time_now = dt.now().strftime('%Y-%m-%d %H:%M:%S') self.attendance[state].append(time_now) return f"{self.name}: {state.replace('_', ' ').capitalize()} at {time_now}!" # 實例化 mary = Worker("Mary", 25, "0123") # 未打卡 mary.staff_info() # 打卡上班 print(mary.clock_in_and_out("clock_in")) mary.staff_info() # 打卡下班 print(mary.clock_in_and_out("clock_out")) mary.staff_info() ``` - 高內聚:差不多的功能是被包在一起的,提高可讀性、易維護性 - 低耦合:不同功能之間比較不會重疊跟影響 - 未封裝:每個函數都要引入一次資料,改一個要改全部 - 有封裝:一開始設定好屬性,之後每個函數都是獨立作業,只要改一個地方即可 - 資料保護: - 公有變數(Public):類別內外都可以自由存取與修改 - 私有變數(Private):僅限於類別內部使用,外部無法直接存取 ==__ID== - 受保護變數(Protected):慣例上只能被類別本身和繼承他的子類別訪問,但外部依然可以存取 ==\_age== ### 2. 繼承 Inheritance 類別之間都有一些重複性的屬性或者方法,不斷重複寫一樣的代碼反而會降低程式碼的可讀性。因此可以用繼承的方式讓 **子類別** 繼承 **父類別/超類** 的屬性或方法(除了私有化的) > 子女會遺傳父母的一些特徵或屬性,例如髮色、膚色、血型等等,但有些還是只有父母保有的特徵,沒有遺傳給小孩 ![image.png](https://miro.medium.com/v2/resize:fit:1400/format:webp/1*9Iqwer5h_WVuAQmnCgBzIw.png) ```python # 父類別 class Person: def __init__(self, name, age, ID): self.name = name self.age = age self.__ID = ID def speak(self, sentence): return self.name + " says " + sentence # 繼承 Person 的類別 class Athlete(Person): def workout(self): return '%s goes to the gym to exercise twice a week.' % (self.name) def running(self): return '%s goes running three times a week.' % (self.name) # 繼承 Athlete 的類別 class Champion(Athlete): def get_trophy(self): return '%s got the champion!' % (self.name) # 實例化 olina = Champion('Olina', 20, '3210') print(olina.speak('Hello World')) # Parent class: Person print(olina.workout()) # Child class: Athlete print(olina.running()) # Child class: Athlete print(olina.get_trophy()) # Grand child class: Champion # ------ output ------ # Olina says Hello World # Olina goes to the gym to exercise twice a week. # Olina goes running three times a week. # Olina got the champion! ``` #### 覆寫(override) 父類別原有的屬性或方法之上再做一點修改,可以用`super()` 的語法 ```python # 父類別 class Person: def __init__(self, name, age, ID): self.name = name self.age = age self.__ID = ID def speak(self, sentence): return self.name + " says " + sentence # 子類別 class Athlete(Person): # 覆寫建構子 def __init__(self, name, age, ID, height): # 新增 height 的屬性 super().__init__(name, age, ID) # 1 self.height = height # 覆寫 speak 方法 def speak(self, sentence): print(super().speak(sentence)) # 2 return "Here is subclass..." # 實例化 james = Athlete('James', 23, '0123', 170) print(james.name) # 繼承屬性 print(james.age) # 繼承屬性 print(james.height) # 新增屬性 print(james.speak("Hello World Again")) # ------ output ------ # James # 23 # 170 # James says Hello World Again # Here is subclass... ``` 1. `super()` 語法需要指定要覆寫的標的,這裡覆寫父類別的建構子並額外新增身高的屬性:`self.height = height`。 2. `super()` 語法覆寫父類別的 `speak` 方法並額外新增 `print('Here is subclass...')` 。 #### 多重繼承 一個子類別可以繼承多個父類別 ```python # 父類別 1 class Father: def say(self): return 'I am a doctor.' # 父類別 2 class Mother: def say(self): return 'I am a lawyer.' # 多重繼承的子類別 class Child(Mother, Father): pass # 實例化 mike = Child() print(mike.say()) # ------ output ------ # I am a lawyer. ``` 1. 優先找子類別裡面是否有此方法 2. 如果沒有就會由左往右的找父類別底下是否有同名的調用方法 ### 3. 多型 Polymorphism 在不同類別底下的方法,即便是取一樣的名字,在實例化後進行呼叫,會產生不同的運行結果,造就多型的體現。 ```python class Leo(): def develope_habbit(self): # 1 return 'Leo likes Basketball.' class James(): def develope_habbit(self): # 1 return 'James likes Tennis.' # 2 leo = Leo() james = James() print(leo.develope_habbit()) # 呼叫 Leo 的 develope_habbit() print(james.develope_habbit()) # 呼叫 James 的同名方法 # ------ output ------ # Leo likes Basketball. # Noah likes Tennis. ``` 1.  `Leo` 和 `James` 兩個類別裡皆有 `develope_habbit` 的方法,但是方法內容不一樣。 2.  分別實例化之後在同時調用同名方法,產生不同結果。 #### 依賴注入 (Dependency Injection) 依賴注入代表不同類別的依賴關係是可以進一步被串接起來,有助於管理類別物件之間的依賴性。也就是說,透過依賴注入的方式可以輕鬆的「組裝」不同類別 ```python # 1 class Basketball: def play(self): return 'play basketball' class Tennis: def play(self): return 'play tennis' # 2 class Student: def __init__(self, name, hobby): self.name = name self.hobby = hobby print('My name is %s and I like to %s.' % (self.name,self.hobby.play())) # 3 def change_hobby(self, hobby): self.hobby = hobby print('%s changes hobby to %s.' % (self.name, self.hobby.play())) def play(self): print('%s %s.' % (self.name, self.hobby.play() )) # 4 實例化 Leo = Student('Leo', Basketball()) Leo.play() Leo.change_hobby(Tennis()) Leo.play() # ------ output ------ # My name is Leo and I like to play basketball. # Leo play basketball. # Leo changes hobby to play tennis. # Leo play tennis. ``` 1. 不同的運動用類別區分開來,裡面都有同名的 `play` 方法 2. 建立`Student` 的類別,並且將建構式的參數 `hobby` 傳入預設的運動類別,代表喜歡的運動,並用 `self.hobby` 存起來 3. 設定改變喜好的方法,傳入不同的運動類別 4. 實例化後,調用 `play()` 來呈現更換的喜好。 :::success 可擴充性:透過將「喜好」分離出來作為可以注入的類別,可以更彈性的更改喜好的轉變,也可以自由地增加更多的喜好。或是更近一步進行擴充,加上喜好之外的類別組合。 ::: #### 抽象類別 (Abstract Class) 一種搭配繼承的方式,藉由將父類別的方法 **抽象化** 來達成,可以讓父類的方法更有彈性地讓子類使用。首先直接定義一個方法名稱,但不實作內部的細節,等於是一個「空」的方法,這一個空的方法稱為 **抽象方法** (abstract method),具有抽象方法的類別稱為 **抽象類別** (abstract class),抽象方法可以用 `raise NotImplementedError` 或 `pass` ```python # 1 父類別 class Person: def __init__(self, name, age): self.name = name self.age = age def speak(self, sentence): return self.name + " says " + sentence # 2 抽象方法 def getHobby(self): raise NotImplementedError # 子類別 1 class Leo(Person): def getHobby(self): return 'I like to play basketball.' # 子類別 2 class James(Person): def getHobby(self): return 'I like to play tennis.' # 3 實例化 leo = Leo('Leo', 20) james = James('James', 23) print(leo.getHobby()) print(james.getHobby()) # ------ output ------ # 'I like to play basketball.' # 'I like to play tennis.' ``` 1. 在父類別 `Person` 內新增 `getHobby()` 抽象方法,不寫入實做細節 2. 在子類別 `Leo` 和 `James` 中為抽象方法 `getHobby()` 定義實作細節,內容是不一樣的 3. 分別實例化之後,調用同名方法就會產生不同結果 ## 程序導向 vs. 物件導向 :::success 物件導向:是一個設計概念、一種思維,任何程式語言都通用 ::: 假設要設計一個一系列的運算器,這個運算器需要經歷以下運算過程: 1. 傳入除數 (divisor) 與被除數 (dividend) 2. 將步驟一的運算結果作為基數 (base),傳入高 (height) 的值,執行三角形的面積運算 3. 將步驟二的回傳值進行最後的運算(平方、開根號...) ### 程序導向寫法 ```python dividend = 100 divisor = 2 height = 5 # Step 1 def divider(dividend, divisor): result = round(dividend/divisor) print(f"Divider: {result}") return result # Step 2 def triangle_area(base, height): area_result = round((base * height) / 2) print(f"Triangle area: {area_result} ") return area_result # Step 3 def square(num): return num * num # Combine the above together to form the calculator def main_calculator(dividend, divisor, height): result = divider(dividend, divisor) area = triangle_area(result, height) final_result = square(area) print(f"Calculator: {final_result} ") return final_result # Execution final_result = main_calculator(dividend, divisor, height) final_result # ------ output ------ # Divider: 50 # Triangle area: 125 # Calculator: 15625 ``` #### 產生的問題: 1. 函數之間的耦合性高:函式 `divider(dividend, divisor)` 以及 `triangle_area(base, height)` 都共享著全域變數。 2. 維護不易:由於 `dividend`、`divisor` 以及 `height` 為全域變數,如果有別的地方名稱一樣就有可能被覆蓋過去。再加上許多函式也都需要將其作為參數傳入,所以後續需要做更改的話,整理更動的地方就會很多。 例:`dividend` 轉為 float 或者 string 等等,帶有這些參數的函式也都需要跟著做修改。 3. 擴充不夠彈性:如果最後一步要做不同的最終運算的話,就需要創建更多函數以及改寫最終 calculator 的運算邏輯,導致程式碼會比較雜亂,也不好維護。 ### 物件導向寫法 ```python class Calculator: def __init__(self, dividend, divisor, height): self.dividend = dividend self.divisor = divisor self.height = height def divider(self): result = round(self.dividend / self.divisor) print(f"Divider: {result}") return result def triangle_area(self): base = self.divider() area_result = round((base * self.height) / 2) print(f"Triangle area: {area_result} ") return area_result def final_excutor(self): raise NotImplementedError # 最終運算用子類別提高運算彈性 class Final_square(Calculator): def final_excutor(self): num = self.triangle_area() final_result = num * num print(f"Final result: {final_result}") return final_result dividend = 100 divisor = 2 height = 5 final_result = Final_square(dividend, divisor, height) final_result.final_excutor() ``` #### 優勢: 1. 變數的汙染降低:當變數傳入類別之後就統一被建構子管理成內部的變數,後續的方法不需再引入變數當參數 2. 維護容易:如果新增功能,只需要再新增一個方法;資料結構改變也只需要更改建構子一個地方的變數就可以了 3. 擴充的彈性提高:藉由繼承以及多型的搭配使得最後的運算步驟可以有很高的彈性,同時也維持住程式碼的可讀性及維護性。 | 特點 | 程序導向(Procedure-Oriented) | 物件導向(Object-Oriented) | | -------------- | ------------------------------ |:--------------------------- | | **資料和行為** | 資料和行為分離,函數操作全局或局部變數 | 資料和行為封裝在物件內 | | **邏輯結構** | 思考底層的資料結構為何,由哪些程序步驟完成,是以功能來做為劃分 | 以大概念作起點,先出現運算器的類別,接著建立屬性及方法來達成功能 | | **思路** | 由下而上 | 由上而下 | | **擴展性** | 修改時可能需要改動多處程式碼 | 擴展或修改只需在類別內部調整 | | **可讀性和維護性** | 直接明了,但隨著功能增加,維護變得複雜 | 結構清晰,尤其在處理複雜系統時便於維護 | | **重用性** | 主要通過複製函數邏輯實現重用 | 物件和類別的繼承與多型可以提高重用性 | | **適用情境** | 適合簡單、小型的專案| 適合大型、複雜的系統 | ## References 1. [[Python]-關於物件導向程式設計 (Object-Oriented Programming, OOP)](https://medium.com/@leo122196/python-%E9%97%9C%E6%96%BC%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88-object-oriented-programming-oop-b3ce7ae019f3#92b4) 2. [[Python]-封裝 (Encapsulation): 物件導向的三大特色之一](https://medium.com/@leo122196/python-%E5%B0%81%E8%A3%9D-encapsulation-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E7%9A%84%E4%B8%89%E5%A4%A7%E7%89%B9%E8%89%B2%E4%B9%8B%E4%B8%80-9196f8aa4ef6#b915) 3. [[Python]-繼承 (Inheritance): 物件導向的三大特色之一](https://medium.com/@leo122196/python-%E7%B9%BC%E6%89%BF-inheritance-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E7%9A%84%E4%B8%89%E5%A4%A7%E7%89%B9%E8%89%B2%E4%B9%8B%E4%B8%80-433891ad8423) 4. [[Python]-多型 (Polymorphism): 物件導向的三大特色之一](https://medium.com/@leo122196/python-%E5%A4%9A%E5%9E%8B-polymorphism-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E7%9A%84%E4%B8%89%E5%A4%A7%E7%89%B9%E8%89%B2%E4%B9%8B%E4%B8%80-a125f3647e6d) 5. [何謂物件導向程式設計](https://hackmd.io/@metal35x/rk2uiTnXI) 6. [物件導向 vs 程序式導向-平庸與高級工程師開發速度差異的秘密](https://zxuanhong.medium.com/%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-vs-%E7%A8%8B%E5%BA%8F%E5%BC%8F%E5%B0%8E%E5%90%91-%E5%B9%B3%E5%BA%B8%E8%88%87%E9%AB%98%E7%B4%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%E9%96%8B%E7%99%BC%E9%80%9F%E5%BA%A6%E5%B7%AE%E7%95%B0%E7%9A%84%E7%A7%98%E5%AF%86-19e6357b54e6) 7. [物件導向武功秘笈(1):認知篇 — 什麼是好的程式?](https://ycc.idv.tw/introduction-object-oriented-programming_1.html#_3) 8. [[程式設計] 10分鐘搞懂物件導向 Object-oriented programming](https://medium.com/@p81122g/%E6%B7%BA%E8%AB%87%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88-object-oriented-programming-81355c85484b) 9. [[Python教學] Class 類別](https://utrustcorp.com/python-class/)