Python 物件導向程式設計(OOP)── 屬性與方法
簡介
在 Python 中,物件導向程式設計(OOP) 是組織程式碼、提升可維護性與可重用性的核心概念。
屬性(Attribute)與方法(Method)是類別(class)最基本的構成單位:屬性用來保存物件的狀態,方法則描述物件能執行的行為。掌握它們的使用方式,才能寫出結構清晰、易於擴充的程式。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者建立 Python OOP 的基礎,適合剛接觸物件導向的初學者,也能為已有經驗的開發者提供實務參考。
核心概念
1. 類別與實例(Class & Instance)
- 類別 是藍圖,定義了物件擁有的屬性與方法。
- 實例(instance)是根據類別產生的具體物件,每個實例都有自己的屬性值。
class Car:
"""一個簡單的車子類別"""
# 類別屬性(所有實例共享)
wheels = 4
def __init__(self, brand, color):
# 實例屬性(每個實例獨有)
self.brand = brand
self.color = color
def drive(self):
"""模擬開車"""
print(f"{self.brand} ({self.color}) 正在行駛...")
# 建立兩個實例
c1 = Car("Toyota", "紅色")
c2 = Car("Honda", "藍色")
c1.drive() # Toyota (紅色) 正在行駛...
c2.drive() # Honda (藍色) 正在行駛...
print(Car.wheels) # 4
2. 實例屬性 vs 類別屬性
| 類別屬性 | 實例屬性 |
|---|---|
| 定義在類別本身,所有實例共用 | 定義在 __init__ 或其他方法中,只屬於該實例 |
| 變更會影響所有實例 | 變更只影響該實例 |
| 常用於常數、計數器等 | 用於描述個別物件的狀態 |
class Counter:
total = 0 # 類別屬性:全域計數
def __init__(self):
Counter.total += 1 # 每產生一個實例,總數加一
self.id = Counter.total # 實例屬性:唯一編號
c1 = Counter()
c2 = Counter()
print(Counter.total) # 2
print(c1.id, c2.id) # 1 2
3. 方法的種類
3.1 實例方法(Instance Method)
最常見的方式,第一個參數固定為 self,代表呼叫此方法的實例本身。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
"""計算矩形面積"""
return self.width * self.height
3.2 類別方法(Class Method)
使用 @classmethod 裝飾器,第一個參數為 cls,代表類別本身。常用於工廠函式或修改類別屬性。
class Person:
population = 0
def __init__(self, name):
self.name = name
Person.population += 1
@classmethod
def get_population(cls):
"""回傳目前人口數"""
return cls.population
3.3 靜態方法(Static Method)
使用 @staticmethod 裝飾器,既不接受 self 也不接受 cls。適合把「與類別相關」但不需要存取類別或實例狀態的函式放在類別命名空間中。
class MathUtil:
@staticmethod
def is_even(n):
"""判斷是否為偶數"""
return n % 2 == 0
4. property:封裝 getter / setter
直接存取屬性雖然簡潔,但在需要驗證、計算或延遲載入時,使用 property 可以保持介面的簡潔,同時加入額外邏輯。
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # 使用「私有」變數儲存實際值
@property
def celsius(self):
"""取得攝氏溫度"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""設定攝氏溫度,同時檢查合理範圍"""
if value < -273.15:
raise ValueError("溫度不能低於絕對零度")
self._celsius = value
@property
def fahrenheit(self):
"""自動轉換為華氏溫度(唯讀)"""
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius) # 25
print(t.fahrenheit) # 77.0
t.celsius = -300 # => ValueError: 溫度不能低於絕對零度
5. 多重繼承與方法解析度順序(MRO)
Python 支援多重繼承,但若同名屬性或方法出現在多個父類別,解析度順序(Method Resolution Order) 決定哪個被呼叫。super() 可協助正確呼叫父類別的實作。
class A:
def greet(self):
print("Hello from A")
class B(A):
def greet(self):
print("Hello from B")
super().greet() # 呼叫 A 的 greet
class C(A):
def greet(self):
print("Hello from C")
super().greet() # 呼叫 A 的 greet
class D(B, C):
pass
d = D()
d.greet()
# 輸出:
# Hello from B
# Hello from C
# Hello from A
常見陷阱與最佳實踐
| 常見問題 | 說明 | 解決方式 |
|---|---|---|
| 屬性名稱衝突 | 類別屬性與實例屬性同名,會導致意外覆寫。 | 盡量使用前綴(如 self._value)或明確區分類別屬性(ClassName.attr)。 |
忘記傳 self |
定義實例方法時忘記第一個參數,呼叫會出錯。 | 讓 IDE 或 lint 工具檢查函式簽名。 |
過度使用 @property |
每個屬性都包成 property,會降低效能且增加程式碼複雜度。 | 只在需要驗證或計算時使用,普通屬性直接公開即可。 |
濫用 @staticmethod |
把與類別無關的函式塞進類別,破壞單一職責原則。 | 若函式獨立於類別,直接放在模組層級。 |
| 多重繼承的 MRO 混亂 | 不熟悉 MRO 會產生「方法不被呼叫」或「重複呼叫」的問題。 | 使用 class MyClass(Base1, Base2): 前先畫出繼承圖,必要時使用 super()。 |
最佳實踐
- 明確區分類別屬性與實例屬性:使用大寫常數命名類別屬性(如
MAX_SPEED = 120),用小寫或_開頭表示私有實例屬性。 - 盡量保持方法短小:每個方法只做一件事,讓單元測試更容易。
- 使用
@classmethod建立工廠方法:有多種初始化方式時,比起在外部寫多個建構子,工廠方法更具可讀性。 - 利用
property隱藏實作細節:對外提供簡潔介面,未來若需改變儲存方式,只要改寫 getter/setter 即可。 - 寫文件字串(docstring):每個類別與方法都加上說明,配合 IDE 的自動補全,提升團隊協作效率。
實際應用場景
1. 網路 API 客戶端
import requests
class GitHubUser:
"""簡易的 GitHub 使用者 API 包裝"""
base_url = "https://api.github.com/users"
def __init__(self, username):
self.username = username
self._data = None # 延遲載入
@property
def data(self):
"""首次存取時才向 GitHub 請求資料"""
if self._data is None:
resp = requests.get(f"{self.base_url}/{self.username}")
resp.raise_for_status()
self._data = resp.json()
return self._data
def repo_count(self):
"""回傳公開倉庫數量(讀取快取)"""
return self.data.get("public_repos", 0)
# 使用
user = GitHubUser("torvalds")
print(user.repo_count()) # 6(依實際情況而定)
透過屬性封裝 API 請求,使呼叫者只需要關注 repo_count(),而不必自行處理 HTTP、快取等細節。
2. 金融交易系統的交易單(Order)
class Order:
"""交易單模型,包含狀態變化的封裝"""
STATUS_NEW = "new"
STATUS_FILLED = "filled"
STATUS_CANCELED = "canceled"
def __init__(self, symbol, qty, price):
self.symbol = symbol
self.qty = qty
self.price = price
self._status = self.STATUS_NEW
@property
def status(self):
return self._status
def fill(self):
if self._status != self.STATUS_NEW:
raise RuntimeError("只能在新單狀態下成交")
self._status = self.STATUS_FILLED
def cancel(self):
if self._status == self.STATUS_FILLED:
raise RuntimeError("已成交的單無法取消")
self._status = self.STATUS_CANCELED
將「只能在特定狀態下」的商業規則寫在方法內,避免外部直接改變 _status,提升系統的資料完整性。
3. GUI 元件的屬性綁定
import tkinter as tk
class CounterLabel(tk.Label):
"""自訂的標籤,顯示計數值並自動更新"""
def __init__(self, master=None, **kw):
super().__init__(master, **kw)
self._count = 0
self.config(text=str(self._count))
@property
def count(self):
return self._count
@count.setter
def count(self, value):
self._count = value
self.config(text=str(self._count)) # UI 同步更新
# 範例使用
root = tk.Tk()
lbl = CounterLabel(root)
lbl.pack()
def inc():
lbl.count += 1
btn = tk.Button(root, text="加一", command=inc)
btn.pack()
root.mainloop()
property 讓 UI 元件的內部狀態與畫面顯示保持一致,呼叫者只需要改變 lbl.count 即可。
總結
- 屬性 用來保存物件的狀態,分為 類別屬性(共享)與 實例屬性(獨立)。
- 方法 定義物件的行為,常見的有 實例方法、類別方法、靜態方法,各自適用不同情境。
@property、@classmethod、@staticmethod為 Python 提供的語法糖,讓程式碼更具可讀性與可維護性。- 熟悉 MRO 與
super(),才能在多重繼承中正確呼叫父類別的實作。 - 在實務開發中,透過屬性封裝、方法驗證與工廠模式,可以把 商業規則、資料驗證、外部資源存取 隱藏於類別內部,讓呼叫端只關心「我要做什麼」而不是「怎麼做」。
掌握了屬性與方法的正確使用方式,你就能在 Python 專案中建立結構清晰、易於測試與維護的物件導向程式碼。祝你寫程式愉快,持續探索 OOP 的無限可能!