# 物件導向程式設計(使用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`