本文 AI 產出,尚未審核

Python 物件導向程式設計(OOP)——封裝(私有變數、protected)


簡介

在物件導向程式設計 (OOP) 中,封裝 是四大基本概念之一(另外三個分別是繼承、抽象與多型)。封裝的核心目的是將資料與操作資料的行為綁在同一個物件內,並透過 存取限制(private、protected)來保護內部狀態不被外部隨意修改。

在 Python 裡,雖然語言本身沒有像 Java 那樣嚴格的存取修飾子,但它提供了 命名慣例(單底線、雙底線)來表達「私有」與「受保護」的意圖。掌握這些慣例不只可以提升程式的可讀性與維護性,還能防止因不當修改導致的錯誤與資安問題。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,直到實務應用情境,完整介紹 Python 中的封裝技巧,幫助初學者到中階開發者快速上手、寫出更健全的程式碼。


核心概念

1. 為什麼需要封裝?

  • 資料隱蔽:把物件內部的狀態隱藏起來,只允許透過特定方法(getter / setter)存取。
  • 降低耦合:外部程式只需要知道介面,不必關心實作細節,未來若要改變內部演算法時不會影響使用者。
  • 提升安全性:防止外部直接改寫關鍵屬性,避免產生不可預期的錯誤或安全漏洞。

2. Python 的存取慣例

修飾子 實際寫法 代表意圖 Python 內部機制
public self.name 公開,任何人皆可直接存取 無限制
protected _name 受保護,建議僅在子類別或同模組內使用 只是一個慣例,仍可直接存取
private __name 私有,僅限於本類別內部使用 會觸發 name mangling(名稱改寫)

注意protectedprivate 並不是語法強制,僅靠開發者自律與團隊規範。

3. Name Mangling(名稱改寫)

當屬性以雙底線開頭 (__) 時,Python 會在編譯階段把它改寫成 _ClassName__attr 的形式,以避免子類別意外覆寫。例如:

class A:
    __secret = 42

外部若直接使用 A.__secret 會得到 AttributeError,必須透過 _A__secret 才能存取,這就是 名稱改寫 的結果。雖然仍可繞過,但已足以提醒開發者「這是私有屬性,請勿直接使用」。


程式碼範例

以下示範 4 個常見的封裝情境,從最基礎的公有屬性,到使用 getter / setter、property、以及繼承時的保護與私有屬性。

範例 1:最簡單的公有屬性

class Person:
    def __init__(self, name):
        self.name = name          # 公有屬性,外部可直接讀寫

p = Person("Alice")
print(p.name)      # Alice
p.name = "Bob"
print(p.name)      # Bob

說明:此寫法最直觀,但缺乏資料驗證與封裝保護。


範例 2:使用單底線表示受保護屬性

class Vehicle:
    def __init__(self, brand):
        self._brand = brand      # 受保護屬性,建議僅在子類別使用

    def get_brand(self):
        return self._brand

class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)
        self.model = model

c = Car("Toyota", "Corolla")
print(c.get_brand())   # Toyota
# 雖然可以直接存取 c._brand,但這違背了慣例

說明:單底線是一種「暗示」而非限制,團隊規範應明確說明不允許外部直接存取 _brand


範例 3:雙底線私有屬性 + getter / setter

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance   # 私有屬性,外部不可直接存取

    # Getter
    def get_balance(self):
        return self.__balance

    # Setter(加入驗證)
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("存款金額必須大於 0")
        self.__balance += amount

    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("餘額不足")
        self.__balance -= amount

acc = BankAccount("張三", 1000)
print(acc.get_balance())   # 1000
acc.deposit(500)
print(acc.get_balance())   # 1500
# 以下會拋出 AttributeError,證明 __balance 為私有
# print(acc.__balance)

說明:透過 getter / setter,我們可以在存取前加入額外的驗證或日誌,避免不合理的操作。


範例 4:@property 讓屬性存取更自然

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        """取得寬度"""
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("寬度必須為正數")
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("高度必須為正數")
        self._height = value

    @property
    def area(self):
        """只讀屬性,計算面積"""
        return self._width * self._height

rect = Rectangle(3, 4)
print(rect.area)   # 12
rect.width = 5
print(rect.area)   # 20
# rect.area = 30   # 會拋出 AttributeError,因為沒有 setter

說明@property 讓使用者感受到「屬性」的語意,同時仍保有封裝與驗證的好處,寫起來更直覺。


範例 5:繼承時的私有屬性衝突(Name Mangling 示範)

class Base:
    def __init__(self):
        self.__value = 10   # 會被改寫成 _Base__value

    def get_base_value(self):
        return self.__value

class Derived(Base):
    def __init__(self):
        super().__init__()
        self.__value = 20   # 改寫成 _Derived__value,與 Base 的私有屬性不衝突

    def get_derived_value(self):
        return self.__value

d = Derived()
print(d.get_base_value())    # 10
print(d.get_derived_value())# 20
# 若直接存取 d._Base__value 仍可取得,但這已違背封裝原則

說明:雙底線的 name mangling 能防止子類別意外覆寫父類別的私有屬性,這在大型系統中相當有用。


常見陷阱與最佳實踐

常見陷阱 可能後果 推薦做法
直接存取雙底線屬性 (obj.__attr) 破壞封裝,未來改名或改寫會導致程式崩潰 使用 getter / setter,或 @property 取得值
在子類別中重新定義同名單底線屬性 (_attr) 可能造成意外的屬性遮蔽 若要覆寫,使用雙底線或明確說明覆寫意圖
過度使用 getter / setter,導致程式碼冗長 失去 Python 「簡潔」的風格 僅在需要驗證、計算或保護時才加入,否則使用公開屬性
忽視 property 的只讀屬性 使用者可能意外改變本該固定的值 為計算屬性或常數使用 @property 且不提供 setter
__init__ 之外直接修改私有屬性 破壞類別內部不變式 把所有變更邏輯集中於方法內,保持單一責任

最佳實踐清單

  1. 遵守命名慣例:單底線 → 受保護;雙底線 → 私有。團隊文件中明確列出規則。
  2. 盡量使用 @property:讓介面更自然,同時保留驗證與計算邏輯。
  3. 將驗證邏輯集中於 setter:避免在多個方法裡重複寫相同檢查。
  4. 保持私有屬性不可變(immutable)或僅在特定方法內變更,提升可預測性。
  5. 寫單元測試:測試 getter / setter 的邊界條件,確保封裝不會因未預期的輸入而失效。

實際應用場景

  1. 金融系統:帳戶餘額、交易紀錄等敏感資料必須以私有屬性保存,並透過嚴格的驗證介面(deposit、withdraw)控制變更。
  2. Web 框架的模型層:ORM(如 Django、SQLAlchemy)會把資料庫欄位封裝為屬性,使用 @property 讓開發者在存取時自動觸發驗證或轉型。
  3. 硬體驅動程式:設備狀態(如連接狀態、緩衝區)不應被外部直接修改,透過封裝提供安全的 API。
  4. 大型套件的插件系統:基底類別使用雙底線隱藏內部實作,插件只能透過公開介面與之互動,避免破壞核心邏輯。
  5. 資料分析 pipeline:把中間結果(如 DataFrame)封裝在類別內,外部只能呼叫 get_result() 取得,防止不小心改寫導致後續步驟錯誤。

總結

  • 封裝 是保護物件內部狀態、提升程式可維護性的關鍵。
  • Python 透過 命名慣例(單底線、雙底線)與 name mangling 來實現受保護與私有屬性。
  • 使用 getter / setter 或更 Pythonic 的 @property,可以在保持簡潔語法的同時加入驗證、計算或只讀限制。
  • 避免直接存取私有屬性、過度設計 getter / setter,並遵守團隊的封裝規範,才能寫出既安全又易於維護的程式碼。

掌握了封裝的正確寫法後,你的 Python 專案將更具彈性、可靠性,也更符合業界對於 乾淨程式碼(Clean Code)的期待。祝你在 OOP 的旅程中,寫出更優雅、更安全的程式!