本文 AI 產出,尚未審核

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 會依照以下順序尋找屬性:

  1. 實例的 __dict__(即實例屬性)
  2. 類別的 __dict__(類別屬性)
  3. 父類別的 __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 包裝修改邏輯。
可變物件作為類別屬性 listdict 放在類別屬性,所有實例共享同一容器,容易產生資料互相污染。 若需要每個實例擁有獨立的容器,應在 __init__ 中建立 self.attr = []。若真的要共享,請明確註解並使用 copy()deepcopy()
忘記使用 self 在建構子或方法裡寫 attr = value,實際上是建立局部變數,實例屬性不會被設定。 必須寫成 self.attr = value
類別屬性命名衝突 同名屬性同時作為類別屬性與實例屬性,容易混淆。 建議使用前綴或不同命名風格,例如 DEFAULT_TIMEOUT(類別屬性) vs timeout(實例屬性)。
多繼承時的屬性解析順序(MRO) 繼承樹較複雜時,類別屬性可能被意外覆寫。 熟悉 Python 的 MRO(Class.__mro__),必要時使用 super() 明確呼叫。

最佳實踐

  1. 將不會變動的常數放在類別屬性,並使用全大寫命名(PEP8)。
  2. 可變物件預設放在 __init__,避免共享副作用。
  3. 若屬性需要被多個實例讀寫,提供 @classmethod@property 包裝存取,以維持封裝性。
  4. 使用 __slots__ 限制實例屬性,可減少記憶體開銷(適合大量小物件情境)。
  5. 寫單元測試,特別是檢查類別屬性變更是否會影響其他實例。

實際應用場景

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 程式碼更具結構性、可讀性與可維護性。祝你寫程式愉快,持續在物件導向的世界裡探索更高階的設計模式!