Python 物件導向程式設計(OOP)─ 類別屬性與實例屬性
簡介
在 Python 的物件導向程式設計中,類別屬性(class attribute)與 實例屬性(instance attribute)是兩種最常見、也是最容易混淆的屬性類型。掌握它們的差異與使用時機,能讓程式碼更具可讀性、可維護性,同時避免不必要的錯誤。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶入實務應用場景,逐步帶你了解什麼時候該使用類別屬性、什麼時候該使用實例屬性,並提供可直接套用的程式碼範例,適合 初學者到中階開發者 閱讀。
核心概念
1. 什麼是類別屬性?
類別屬性是屬於 整個類別本身 的變數,所有由該類別產生的實例(object)都會共享同一份資料。換句話說,修改類別屬性會同步影響所有已建立或未來建立的實例。
class Car:
# 類別屬性:所有 Car 共享的資訊
wheels = 4 # 四輪車的預設值
manufacturer = "Toyota"
# 兩台不同的 Car 實例
c1 = Car()
c2 = Car()
print(c1.wheels, c2.wheels) # 4 4
# 變更類別屬性
Car.wheels = 6
print(c1.wheels, c2.wheels) # 6 6
重點:
Car.wheels是類別屬性,c1.wheels只是一個「查找」的過程,若實例本身沒有同名屬性,就會去類別中找。
2. 什麼是實例屬性?
實例屬性屬於 單一物件,每個實例都有自己的獨立資料。通常在 __init__ 建構子裡以 self. 開頭定義。
class Car:
def __init__(self, color, model):
# 實例屬性:每台車都有自己的顏色與型號
self.color = color
self.model = model
c1 = Car("red", "Corolla")
c2 = Car("blue", "Camry")
print(c1.color, c2.color) # red blue
c1.color = "black"
print(c1.color, c2.color) # black blue
提示:實例屬性只能透過
self存取,且不會影響同類別的其他實例。
3. 何時使用類別屬性?
- 常數或預設值:例如「全域設定」或「不會因個別實例而改變」的資料。
- 計數器:想要追蹤已建立多少個實例時,可用類別屬性累加。
- 共享資源:如資料庫連線、緩存(cache)等,所有實例共用同一份資源。
class User:
# 類別屬性:全域使用者計數
total_users = 0
def __init__(self, name):
self.name = name
User.total_users += 1 # 每建立一個實例,計數加一
u1 = User("Alice")
u2 = User("Bob")
print(User.total_users) # 2
4. 何時使用實例屬性?
- 個別狀態:每個物件都有自己的資料,如「使用者名稱、年齡、購物車內容」等。
- 需要在建構子裡根據參數動態決定的值。
- 避免不必要的共享:若屬性不該被其他實例看到或改動,應使用實例屬性。
5. 類別屬性與實例屬性的搜尋順序
Python 會依照以下順序尋找屬性:
- 實例的
__dict__(即實例屬性) - 類別的
__dict__(類別屬性) - 父類別的
__dict__(繼承而來的類別屬性)
若在實例中重新賦值同名屬性,會「遮蔽」(shadow)類別屬性,形成新的實例屬性。
class Demo:
shared = 10 # 類別屬性
d = Demo()
print(d.shared) # 10 (從類別找)
d.shared = 20 # 在實例 d 中建立同名的實例屬性
print(d.shared) # 20 (實例自己的值)
print(Demo.shared) # 10 (類別本身未變)
程式碼範例
範例 1:使用類別屬性作為預設參數
class Rectangle:
# 類別屬性:預設的單位長度(公分)
unit = "cm"
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
r = Rectangle(5, 3)
print(f"面積:{r.area()} {Rectangle.unit}") # 面積:15 cm
範例 2:計算物件總數的類別屬性
class Animal:
total = 0 # 類別屬性:所有動物的總數
def __init__(self, species):
self.species = species
Animal.total += 1
@classmethod
def get_total(cls):
return cls.total
cat = Animal("Cat")
dog = Animal("Dog")
print(Animal.get_total()) # 2
範例 3:實例屬性與類別屬性同名的遮蔽行為
class Counter:
count = 0 # 類別屬性
def __init__(self):
self.count = 100 # 實例屬性遮蔽
c1 = Counter()
c2 = Counter()
print(Counter.count) # 0 (類別屬性未變)
print(c1.count) # 100
print(c2.count) # 100
範例 4:共享緩存(Cache)— 類別屬性實作
class ConfigCache:
# 類別屬性:全域緩存字典
_cache = {}
@classmethod
def get(cls, key, default=None):
return cls._cache.get(key, default)
@classmethod
def set(cls, key, value):
cls._cache[key] = value
# 任何地方都可以存取同一份緩存
ConfigCache.set("api_url", "https://example.com/api")
print(ConfigCache.get("api_url")) # https://example.com/api
範例 5:使用 __slots__ 限制實例屬性(提升效能)
class Point:
__slots__ = ("x", "y") # 只允許這兩個實例屬性
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
# p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 類別屬性被意外改寫 | 在實例上直接賦值同名屬性會產生遮蔽,導致其他實例仍使用舊值。 | 若真的要改變全體行為,請直接改 ClassName.attr,或使用 @classmethod 包裝修改邏輯。 |
| 可變物件作為類別屬性 | 如 list、dict 放在類別屬性,所有實例共享同一容器,容易產生資料互相污染。 |
若需要每個實例擁有獨立的容器,應在 __init__ 中建立 self.attr = []。若真的要共享,請明確註解並使用 copy() 或 deepcopy()。 |
忘記使用 self |
在建構子或方法裡寫 attr = value,實際上是建立局部變數,實例屬性不會被設定。 |
必須寫成 self.attr = value。 |
| 類別屬性命名衝突 | 同名屬性同時作為類別屬性與實例屬性,容易混淆。 | 建議使用前綴或不同命名風格,例如 DEFAULT_TIMEOUT(類別屬性) vs timeout(實例屬性)。 |
| 多繼承時的屬性解析順序(MRO) | 繼承樹較複雜時,類別屬性可能被意外覆寫。 | 熟悉 Python 的 MRO(Class.__mro__),必要時使用 super() 明確呼叫。 |
最佳實踐
- 將不會變動的常數放在類別屬性,並使用全大寫命名(PEP8)。
- 可變物件預設放在
__init__,避免共享副作用。 - 若屬性需要被多個實例讀寫,提供
@classmethod或@property包裝存取,以維持封裝性。 - 使用
__slots__限制實例屬性,可減少記憶體開銷(適合大量小物件情境)。 - 寫單元測試,特別是檢查類別屬性變更是否會影響其他實例。
實際應用場景
1. Web 框架的全域設定
在 Django、Flask 等框架中,常會有「全域設定」或「環境變數」這類資訊,適合使用類別屬性儲存,所有請求處理器皆可直接讀取。
class Settings:
DEBUG = True
DATABASE_URI = "sqlite:///app.db"
2. 遊戲開發中的資源緩存
遊戲常需要載入大量圖像、音效檔案。將已載入的資源放入類別屬性緩存,可避免重複讀檔,提升效能。
class SpriteCache:
_cache = {}
@classmethod
def load(cls, path):
if path not in cls._cache:
cls._cache[path] = pygame.image.load(path)
return cls._cache[path]
3. 企業系統的使用者追蹤
在大型系統裡,常會需要即時統計「目前線上使用者數」或「總註冊人數」,使用類別屬性作計數器最簡潔。
class Session:
active_sessions = 0
def __init__(self, user_id):
self.user_id = user_id
Session.active_sessions += 1
def close(self):
Session.active_sessions -= 1
4. 機器學習模型的共享參數
訓練過程中,模型的超參數(learning rate、batch size)通常在所有訓練實例間共享,可放在類別屬性,方便在不同程式模組間統一調整。
class NeuralNet:
learning_rate = 0.001
batch_size = 64
# 其他實作...
總結
- 類別屬性是整個類別共享的資料,適合放 常數、全域設定、計數器、共享資源。
- 實例屬性屬於單一物件,用來保存 個別狀態、建構子參數。
- 了解 Python 的 屬性搜尋順序,才能避免遮蔽與意外共享的問題。
- 避免在類別屬性中放置可變物件,或在實例上不小心改寫類別屬性;必要時使用
@classmethod、@property、__slots__來加強封裝與效能。
掌握這兩種屬性的差異與正確使用方式,將讓你的 Python OOP 程式碼更具結構性、可讀性與可維護性。祝你寫程式愉快,持續在物件導向的世界裡探索更高階的設計模式!