# 物件導向程式設計(使用Python) --- [TOC] --- ## [1] 類別(class)與物件(object) 如果說物件是塑膠盒,類別就像是製造那個塑膠盒的模具。 如果要建立一個新物件,你必須先定義一個類別來指出它含有什麼。舉例而言,如果你想要定義代表人資訊的物件,用每個物件代表一個人,那你要先定義一個人的類別,將它當成模具。 物件裡面有變數(稱為屬性(attribute))以及函式(稱為方法(method)),接下來都會舉例。 ### 定義一個類別 ```python= class Person: pass p = Person() ``` 範例中呼叫 `Person()` 建立物件。 ### 屬性 屬性是在類別或物件裡面的變數,可以是任何其他物件。可以在建立或執行過程中指派。 像這樣,將一些屬性指派給 p: ```python= class Person: pass p = Person() p.age = 18 print(p.age) ``` (`pass` 是用來指出類別是空的) 執行會得到結果: ``` 18 ``` 像這樣簡易的物件也可以儲存多個屬性。 ### 初始化&方法 如果你想在建立過程中指派屬性,你必須用`__init__()`初始化。(但類別定義式不一定需要`__init__()`方法。) 像這樣: ```python= class Person: def __init__(self): self.age = 18 def printAge(self): print(self.age) p = Person() p.printAge() ``` 其中的 `self` 是第一個參數,每個在類別裡的函式定義時第一個參數都必須是它。不過呼叫的時候不需要傳入這個引數。實際使用就像上面的 `printAge()` 函式。 執行後會得到結果: ``` 18 ``` 你也可以在 `__init__()` 函式中傳入參數。 像這樣: ```python= class Person(): def __init__(self, name): self.name=name p = Person("Bob") print(p.name) ``` 執行後會得到結果: ``` Bob ``` --- 接下來介紹物件導向三大特性:封裝(encapsulation)、繼承(inheritance)、多型(polymorphism)。 --- ## [2] 封裝(encapsulation) 將程式實作過程及資料包裝起來,不讓使用者看見時做的細節,同時避免外界直接更改資料,不僅確保程式安全性,也讓程式碼的可讀性更高、更加容易維護。(近似於一種權限控制) Python 跟 C++、Java 不同在沒有私有屬性,但你可以用在屬性前加入雙底線(__),讓類別定義式外面無法直接存取。 --- ## [3] 繼承(inheritance) 用既有的類別建立一個新的類別,沿用舊類別的所有程式碼,也可以添加或改變,而不用重複實作相同的功能。 ### 從父類別繼承 原始的類別稱為父類別(parent,也稱基礎類別(base class)),新類別稱為子類別(child,也稱衍生類別(derived class))。 定義子類別的作法是在 class 名稱後面加括號,裡頭放入父類別名稱,像接下來這樣: ```python= class Weapon: def method(self): print("weapon\'s method.") class Sword(Weapon): pass s = Sword() s.method() ``` 會得到結果: ``` weapon's method. ``` 除了定義的時候須在後面加上父類別名稱外,不需要做額外的操作就能夠讓 `Sword` 從 `Weapon` 繼承到 `method()` 方法。 ### 添加方法 子類別也可以添加父類別沒有的方法。 用以下的例子說明: ```python= class Weapon: pass class Sword(Weapon): def method(self): print("sword\'s method.") s = Sword() s.method() w = Weapon() w.method() ``` `Sword` 物件可以反應對 `method()` 方法的呼叫,但 `Weapon` 物件不行(沒有這個函式),所以我得到了以下結果: ``` sword's method. Traceback (most recent call last): File "C:\Users\boki9\OneDrive\文件\test.py", line 10, in <module> w.method() AttributeError: 'Weapon' object has no attribute 'method' ``` ### super( ) 如果想要取得父類別的幫助,呼叫父類別的方法,就需要用到 `super()` 。 如果你為子類別寫了一個 `__init__()` 方法,會取代掉原本父類別的 `__init__()` 方法。而 super( ) 常見的用途之一正是在 `__init__()` 中,用來確保父類別已經正確地初始化。 舉例而言: ```python= class Mail: def __init__(self,sender,recipent): self.sender=sender self.recipent=recipent class Email(Mail): def __init__(self,sender,recipent,subject): super().__init__(sender,recipent) self.subject=subject e=Email("Bob","Alice","Request") print("sender:",e.sender,", recipent:",e.recipent) print("subject:",e.subject) ``` 會得到: ``` sender: Bob , recipent: Alice subject: Request ``` `super().__init__()` 會呼叫 `Mail.__init__()` 方法,把引數傳遞過去做出指定工作,`self.subject=subject` 這行則是新添加的。 雖然也可以直接寫新的,但這會使得繼承變得難以使用。而且使用 `super()` 的話,一旦父類別的定義被改變了,子類別可以反應所做的改變,可以更好地繼承。如果子類別有新的功能,但會使用到父類別的內容,就用 `super()` 吧。 ### 多重繼承 物件可以繼承多個父類別。當類別使用不屬於自己的方法或屬性,Python 會查看它的父類別們。 對每個你定義的類別,Python 都會計算出一個稱為方法解析順序(method resolution order, MRO)的串列,會由最左邊開始,從左至右處理,直到找到第一個匹配的屬性。每個類別都有一個稱 `mro()` 的方法,會回傳一串類別,按照那些類別尋找物件的方法或屬性。 如下: ```python= class Grandfather: pass class Grandmother: pass class Father(Grandfather,Grandmother): pass class Mother(): pass class Me(Father,Mother): pass print(Me.mro()) ``` 會印出: ``` [<class '__main__.Me'>, <class '__main__.Father'>, <class '__main__.Grandfather'>, <class '__main__.Grandmother'>, <class '__main__.Mother'>, <class 'object'>] ``` 查看的順序也就是這樣: 物件本身 → 物件的類別 → 該物件的第一個父類別 → 第一個父類別的父類別們 → 該物件的第二個父類別,直到找到第一個匹配的屬性。 其實就是根據下列三個條件檢查: 1. 子類別會在父類別之前檢查 2. 多重的父類別會根據列出的順序檢查 3. 如果有兩個選擇時,會挑第一個父類別中的那個 --- ## [4] 多型(polymorphism) 透過相同操作介面來操作不同型別的物件,分為覆寫(overriding)和多載(overloading)。 ### 覆寫(overriding) 用子類別去覆蓋父類別的成員函數,進而達到擴充、加強效果。 子類別最初會從父類別繼承任何東西,接下來我們要試著覆寫它們。 ```python= class Animal: def __init__(self,name): self.name = name def talk(self): return "OAO" class Cat(Animal): def talk(self): return "Meow~" class Dog(Animal): def talk(Self): return "Woof!" animals=[Animal("Animal"),Cat("Cat"),Dog("Dog")] for animal in animals: print(animal.name+" : "+animal.talk()) ``` 執行的結果如下: ``` Animal : OAO Cat : Meow~ Dog : Woof! ``` 雖然引數跟父類別一樣,但子類別的物件會選擇子類別的函式執行。 ### 多載(overloading) #### 函數多載 同名字的函數中,根據參數型態或數量不同而決定呼叫哪個函式。 但在 Python 中不支援其他語言中的多載,如果同時定義了兩個函式在同一個空間中,後者會覆蓋掉前者。Python 不須依型態不同選擇不同的函式,而隨參數數量改變的概念可以用預設引數解決。 例如: ```python= def sum(a,b,c=0): print(a+b+c) sum(1,2) sum(1,2,3) ``` 會得到: ``` 3 6 ``` ###### [附錄:收集位置引數](https://hackmd.io/05b3GiYtTgKIJPEEhWjSnA?view#6-%E9%99%84%E9%8C%84) #### 特殊方法 只要事先定義,可以用內建的型態處理更多東西,例如 Python 的字串可以用 `+` 運算子串接,用 `*` 來做重複。這些特殊方法開頭跟結尾都有雙底線。 舉例來說,如果原先要比較兩個單字是否相同(忽略大小寫),我們可能會這樣寫: ```python= class Word(): def __init__(self,text): self.text = text def equals(self,other): return self.text.lower() == other.text.lower() first = Word("HA") second = Word("ha") third = Word("he") print(first.equals(second)) print(first.equals(third)) ``` (`lower()` 函式將所有字元改成小寫) 會得到: ``` True False ``` 但是,如果想要用一般的 `first==second` 來比較,我們能怎麼辦? 答案是將剛剛的 `equals()` 改成特殊名稱 `__eq__()`,如下: ```python= class Word(): def __init__(self,text): self.text = text def __eq__(self,other): return self.text.lower() == other.text.lower() first = Word("HA") second = Word("ha") third = Word("he") print(first==second) print(first==third) ``` 如此一來,可以用看起來更直覺也更加簡便的方式解決同樣的問題。 這是一些常見的特殊方法: ![](https://i.imgur.com/UoNGrS3.jpg) ![](https://i.imgur.com/gx1b39s.jpg) 可以在此查詢所需的特殊方法: https://docs.python.org/3/reference/datamodel.html#special-method-names 你也可以用特殊方法讓將輸出改為你想要的樣子。 想要改變字串的表示,就定義 `__str__()` 與 `__repr__()` 方法。 `__repr__()` 方法會回傳一個實體的程式碼表示值,也就是該實體確切的內容,而`__str__()` 方法則是轉為字串,也就是 `str()` 和 `print()` 會產生的內容,是為了增加可讀性,兩者都可以簡化除錯跟輸出的過程(如果沒有定義 `__str__()`,就會使用 `__repr__()` 的輸出)。 例如: ```python= class People: def __init__(self,x,y): self.x=x self.y=y def __str__(self): return "{0.x},{0.y}".format(self) def __repr__(self): return "Pair({0.x},{0.y})".format(self) p=People(1,2) print(p) ``` (這裡的 0 是用來指定引數,其實就是 `self` 實體) 會印出: ``` 1,2 ``` 如果是跟直譯器互動的話,直接去檢視 p 值會看到 `__repr__()` 函式回傳的值,像執行上述程式後(除了最後的 `print()` )會得到這樣的結果。 ```python= In [2]: p Out[2]: Pair(1,2) ``` 像是用來格式化字串的輸出的 `format()` 也可以用 `__format__()` 方法支援自訂的格式化動作。有興趣可以嘗試看看。 --- ## [5] 方法型態 ### 實例方法 截至目前為止都是這種方法。實例方法的第一個引數是 `self`,呼叫時,Python 會將該物件傳給這個方法。 ### 類別方法 類別方法會影響整個類別,對其做得任何改變都會影響它的所有物件。 在定義式裡,加上 `@classmethod` 裝飾器就代表它緊接著的函式是類別方法。第一個參數放的是類別本身,傳統上會稱 `cls`。 ### 靜態方法 它既不影響類別,也不影響類別的物件。前面加上 `@staticmethod` 裝飾器,不需要 `self` 或 `cls` 參數。不需要建立這個類別的物件就可以使用了。它就只是在那裡。 ```python= class Demo: @classmethod def class_test(cls): print("class name:", cls.__name__) @staticmethod def static_test(): print("static OAO") d=Demo.class_test() s=Demo.static_test() ``` 執行結果如下: ``` class name: Demo static OAO ``` 差別在於,類別方法可以存取或改變 class 的狀態,但靜態方法不行。 --- ## [6] 附錄 ### 收集位置引數 如果想要把多個引數傳入一個函式做為參數,可以考慮使用這個方式。 「*」可以將數量不定的引數組成一個 tuple。 範例如下: ```python= def print_args(*args): for i in args: print(i,end=" ") print() print_args(1) print_args(1,2,3) ``` 適合用來編寫引數數量不一定的函式。(如果你需要傳入其它引數,要把它們寫在前面,`*args` 放在最後) --- ###### tags: `ckefgisc23rd`