---
title: Python 類別筆記-1
tags: python note
---
# Python 類別筆記(一)
## 筆記資訊
### 系列
Python 類別筆記
第一篇:認識類別
### 撰寫日期
此筆記撰寫日期為 **02/06/2022**。
### 開發環境
屆時,最新的Python版本為 `3.10.2`。
我使用的編輯環境為 Visual Studio Code。
## 前言
### 關於內容
此筆記內容涵蓋所有與 Python 類別相關的知識,由淺入深,並由生動例子使讀者明白為什麼要使用類別以及如何使定義類別。因此,此筆記亦適用於剛認識類別的朋友。
這份筆記的內容會包含各種來自不同網站的資訊及我個人的見解甚至建議,讀者可以自己斟酌!
因為只有一個人撰寫,即使力求完美,也可能有些許錯誤。如果有發現任何錯誤請不要吝嗇聯絡我,謝謝!
### 撰寫動機
為什麼特別針對「類別」寫了一份筆記(一篇文章)?
因為我個人認為這是初學者在學習程式設計(Programming Design)這條路上,最可能是坎坷卻最為重要的。
類別,是物件導向程式設計的基礎、良好資訊系統的必要元素,更是利用標準、第三方函式庫內各種工具的入場券。若能透徹理解這個主題,並正確甚至乾淨地使用在你的程式碼中,這將會成為區分你身為程式設計師與其他人的一點。
### 讀前須知
此筆記可能會有許多專有名詞,以及部分觀念需要有先備知識才可完全理解。
例如:變數、函式、資料型態、參照(Reference)等
因此,建議你先釐清程式設計中變數、函式的觀念,並熟悉Python的相關基礎語法,再來閱讀此筆記,你會有更好的收穫。
## 正文
### 什麼是類別?
#### 定義
> Classes are for coupling state and functionality. You've got some data and some actions you'd like to perform on that data. You want to bundle those two concepts together. That's what a class is for in Python. [name=Trey Hunner]
> 類別(英語:class)在物件導向程式設計中是一種物件導向電腦程式語言的構造,是建立物件的藍圖,描述了所建立的物件共同的特性和方法。[name=Wikipedia]
類別是包含多筆資料、擁有多種功能的物件之「藍圖」。
我們可以在一個類別內定義一個或多個變數以及函式,而這些定義將成為該類別實例(物件)的行為規則。
#### 定位
同時,類別也是走向[物件導向程式設計](https://zh.wikipedia.org/wiki/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)的第一步。
#### 名詞
我們稱在類別中的變數為成員變數;在類別中的函式為成員函式。
而他們也有自己的專有名詞,方便我們更精確、簡短的指涉。
- **成員變數 (Member Variable)** $\iff$ **屬性 (Attribute)**
- 可以想像成「**資料 (Data)**」
- **成員函式 (Member Function)** $\iff$ **方法 (Method)**
- 可以想像成「**功能 (Functionality)**」
同個類別的實例**只會且一定會有**在類別定義中的所有屬性及方法,但他們的值可能在實例之間有差異、變化。
然而,有一種成員是一個類別的所有實例共用的,或者說他隸屬於**類別**而非屬於任何**單一實例**。因此,該種成員只有單一且唯一的值。
他就是:**靜態成員 (Static Member)**
你可以把「靜態」想像成一個修飾用的形容詞。因此,**變數與函式都可以是靜態的**。
### 為什麼要使用類別?
這個問題是最多初學者會有的問題,但其實這個問題有非常多的答案,以下舉出「其中數個」最多人公認的原因。
- 重複使用程式碼,使程式變得乾淨
- 增強程式模組化,使程式更易懂、易維護、易擴充
- 將零散資料封裝成單一物件
- 維持外部介面的簡潔,令使用者更方便
- 隱藏不需或不可讓外部存取的資料
### 如何定義類別?
在 Python 中,若只是單純宣告一個類別,其語法相對簡單。
透過關鍵字 `class` 加上你喜歡的類別名稱,最後以 `:` 結尾,以下的程式區塊即為該類別的定義內容。
我們來定義一個銀行帳戶類別。
```python
class Account:
pass
```
像這樣,我們很輕鬆地宣告了一個類別,但目前他並沒有任何定義。
這代表該類別實例沒有任何「資料」(成員變數;屬性)、沒有任何「功能」(成員函式;方法)嗎?
在下個區塊,我將向你展示一個看似「沒有定義」的類別實例究竟有沒有任何「資料」與「功能」。
### 什麼是實例化?
你可以將類別想像成建築物的設計藍圖。
當我們只是定義了一個類別,並不存在任何實例。
當一個設計師只是畫好藍圖,基於此藍圖所建設的建築物並不存在。
我們必須**實例化**類別,以取得實例(物件)。
設計師須將藍圖交給建築師,依據指示**蓋出建築物**。
同個類別的實例擁有**相同的成員**,但其**成員的值可以不同**。
基於同個藍圖所蓋出的建築物擁有**相同的規格、架構**,但每個建築物的**建設地點、建設成本、材料、重量可能不同**。
### 如何實例化類別?
在 Python 中,實例化一個類別再簡單不過了,其語法為:
`類別名稱(引數)`
接續以上的類別定義,我們在 `main` 函式中實例化 `Account` 類別。
我們並沒有給 `Account` 類別任何定義。因此,實例化不需要傳入任何引數。
```python
account = Account()
```
現在, `account` 就是一個儲存 `Account` 類別實例的記憶體空間之[參照(reference)](https://zh.wikipedia.org/wiki/%E5%8F%83%E7%85%A7)。
或者,簡單來說, `account` 就是 `Account` 的實例。
我們可以透過印出這個物件,來簡單證明這個事實。
```python
print(account)
```
:::info
**執行結果**
`<__main__.Account object at 0x1027767a0>`
:::
`__main__.Account` 代表該參照的資料之**型別**
> `__main__` 代表被執行的檔案
>
> 若該類別是自外部檔案導入進來的, `__main__` 將變成其檔名減去 `.py`
`object` 代表他是個**物件**
`0x1027767a0` 代表該參照所指向的記憶體空間之地址。因此,每次執行可能有不同結果。
### 物件的內建成員
接續上個我們在**宣告**區段提出的問題。
> 像這樣,我們很輕鬆地宣告了一個類別,但目前他並沒有任何定義。
> 這代表該類別實例沒有任何「資料」(成員變數;屬性)、沒有任何「功能」(成員函式;方法)嗎?
抱歉暴雷,先把答案說在前:
答案是一個大大的「**否**」。
接著,我會依序向你展示這個答案的**證明**以及**原因**。
### 怎麼知道物件的內建成員存在?
要驗證這個答案非常簡單。
我們可以透過[Python內建函式(Built-in Functions)](https://docs.python.org/3/library/functions.html)中的 `dir` 函式來檢視一個物件所擁有的所有成員。
```python
print(dir(account))
```
:::info
**執行結果**
`['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']`
:::
以上,就是所有內建的物件成員。
是的,比你想像中的多了滿多,是吧?
是不是因為這些長相奇怪的名字而畏懼學習了呢?
請千萬不要被這些命名給嚇跑了,他們不過是一種命名方式罷了,把神智專注在英文字的意義就好。
這裡針對其中幾個較好理解的成員做解釋
- `__init__`:成員函式
- init $\iff$ **init**ialization
- 類別的建構式(Constructor),後面會深入解釋。
- `__dict__`:成員變數
- dict $\iff$ **dict**ionary
- 該物件的等價字典(Dictionary)
- `__repr__`:成員函式
- repr $\iff$ **repr**esentation
- 回傳能夠表示該物件的字串
- `__eq__`:成員函式
- eq $\iff$ **eq**ual
- `==` 運算
- `__ne__`:成員函式
- ne $\iff$ **n**ot **e**qual
- `!=` 運算
- `__gt__`:成員函式
- gt $\iff$ **g**reater **t**han
- `>` 運算
- `__ge__`:成員函式
- ge $\iff$ **g**reater than or **e**qual to
- `>=` 運算
- `__lt__`:成員函式
- lt $\iff$ **l**ess **t**han
- `<` 運算
- `__le__`:成員函式
- le $\iff$ **l**ess than or **e**qual to
- `<=` 運算
你會發現,這些成員的命名規則都是相同的,都以 `__` 開頭、結尾,其中英文則為實際含義之簡寫。
實際上,這種命名方法有專有名稱,這些內建的方法叫做 Dunder Methods 或是 Magic Methods。
Dunder 為 Double Underscore 的簡稱。以此種命名方式命名的成員的共同特色為「內建」。Python開發團隊為了不佔用開發者的命名空間,刻意採這個幾乎不會有人想用的命名方式。
舉出這些並不是要你一一詳讀並熟記。這是讓你知道,在他們醜陋、難懂的外表下,其實只是簡單的內容。
因此,舉出一些例子讓你了解這些方法並不像他們看起來的那麼難懂。希望這樣有幫助到你減緩剛剛看到那一大串的恐懼。
不過,如果你真的很想要現在就搞懂每個成員的意義及功能,除了善用網路資源,你也可以透過另一個內建函式 `help` 取得官方的說明。
```python
print(help(account.__str__))
```
我就不一一講解、展示了。
### 物件的內建成員來自哪裡?
#### 答案
要能夠解答這個問題,我們就必須先提到一個物件導向程式設計中的觀念--**繼承**。
這個問題的簡答是:
這些成員是繼承 Python 物件之母 `Object` 類別來的。
但這裡並不深入解釋繼承,也不展示 Python 中的繼承語法。
#### 淺談繼承
但,究竟*繼承*是什麼?
現階段,你可以先把繼承行為想成擴充一個既有的類別。繼承他人的類別稱作**子類別**;被繼承的類別稱作**母(父)類別**。之所以說是「擴充」,是因為在繼承母類別後,開發者可以針對子類別進行屬性、方法的**修改**及**增加**。
白話來說,**子類別擁有所有母類別所擁有的成員,但子類別擁有的成員不一定存在於其母類別**。
如果你有學習其他語言的經驗,那聽到剛剛那句話,你可能有些不認同。因為在某些語言,類別存在「存取限制」,而繼承行為也可以指定繼承成員的範圍。
但在Python中,所有成員是**實質上**的公開成員 (Public Members) 。文章後段也會讓你認識在 Python 中的「準私有」成員。
### 如何定義方法?
在類別中定義方法(成員函式)的語法與定義一般函式沒有任何區別,但有兩個地方能夠區分出兩者。
- 方法是定義在類別之下;函式不隸屬任何類別
- (非靜態)方法必接收一個固定參數:self
我們以最基本的建構式當作範例,讓我們來定義先前 `Account` 類別的建構式吧。
```python
class Account:
def __init__(self) -> None:
pass
```
如此,我們就成功地幫 `Account` 類別定義了一個沒有內容的建構式。
#### 什麼是self?
self 是該類別呼叫此方法的實例參照
一個類別的實例可以有很多個,因此我們需要 `self` (一個實例的參照),使方法知道要對哪個實例操作。
可以同時有多棟建築物基於同一張藍圖,因此我們需要建築物的地址,內裝公司才知道要去哪一棟建築物裝修。
##### 類比
如果你學習過C++的類別,那你應該知道 `this` 。
如果你了解 `this` ,那你可以把 `self` 想像成 `*this` 。
### 如何定義屬性?
在傳統電腦科學中,類別屬性的定義要在建構式中。
但事實上,在Python中,你可以在「**任意處**」定義或新增成員變數。
不過,養成良好的習慣,將所有屬性定義集中在建構式,會使你成為一個好的軟體工程師。
無論如何,在Python中新增一物件屬性的語法非常簡單、直覺,只需在物件後加上 `.屬性名稱` 並賦予一值即可。
#### 在建構式中定義屬性
以下還是以正格,在建構式中新增屬性做為範例。
在 `Account` 類別中新增 `balance` 、 `owner_name` 屬性
```python
class Account:
def __init__(self, owner_name: str) -> None:
self.balance: float = .0
self.owner_name: str = owner_name
```
我們來試著再次實例化類別。
```python
account1 = Account('Ruby')
account2 = Account('Tim')
account1.balance += 50.
print(account1.owner_name)
print(account1.balance)
print(account2.owner_name)
print(account2.balance)
```
如果你有這個疑問:為什麼我不用呼叫建構式,但卻要傳入字串作為引數?
因為呼叫 `Account` 是實例化的動作,而他會自動呼叫建構式,並以你所傳入的所有引數做為參數。
如果你有興趣,可以上網查查看關於 `__new__` 方法,他被自動呼叫的時間其實比建構式還來得早,是生成物件的方法。
#### 在其他地方新增物件屬性
你可以對任意物件新增任意屬性,像是這樣。
```python
account = Account('Somebody')
account.id = '1241221241285125'
print(account.id)
```
但再次提醒,這是不好的習慣,你只要知道可以這樣做就好了。
### 範例程式碼
原本想要把所有有關類別的筆記寫在同個文章,但寫到現在發現有點太長了...
所以我決定這篇就以一個運用到以上講解的內容的範例來作為總結,下一篇我們再進入進階的類別內容。
回到我們先前取的例子,銀行帳戶。
我們來實作一些現實生活中銀行帳戶會有的功能與屬性吧。
```python
class Account:
def __init__(self,
owner_name: str,
initial_balance: float=.0
) -> None:
self.owner_name: str = owner_name.strip()
self.balance: float = initial_balance
self.id: int = id(self)
def deposit(self,
amount: float
) -> bool:
if amount > 0:
self.balance += amount
return True
else:
return False
def withdraw(self,
amount: float
) -> bool:
if amount > 0:
self.balance -= amount
return True
else:
return False
```
如此我們就成功第實作出一個有提款、存款功能的銀行帳戶類別。我們來試著實例化看看。
```python
def main() -> int:
my_account = Account('Ruby', 1000)
his_account = Account('Tim', 500)
if my_account.deposit(500):
print('存款成功')
else:
print('存款成功')
if his_account.withdraw(1000):
print('提款成功')
else:
print('提款失敗')
if __name__ == '__main__':
status_code = main()
print(f'[System] Program terminated with a status code of {status_code}')
```
我們下篇文章會對 `Account` 類別作出改善,例如:將 `balance` 屬性設為準私人屬性等等。那我們就下篇文章見吧!
## 參考資料 References
- [Wikipedia](https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5)
- [What is a class? By Trey Hunner](https://www.pythonmorsels.com/topics/what-is-a-class/)