Python 物件導向程式設計(OOP)——封裝(私有變數、protected)
簡介
在物件導向程式設計 (OOP) 中,封裝 是四大基本概念之一(另外三個分別是繼承、抽象與多型)。封裝的核心目的是將資料與操作資料的行為綁在同一個物件內,並透過 存取限制(private、protected)來保護內部狀態不被外部隨意修改。
在 Python 裡,雖然語言本身沒有像 Java 那樣嚴格的存取修飾子,但它提供了 命名慣例(單底線、雙底線)來表達「私有」與「受保護」的意圖。掌握這些慣例不只可以提升程式的可讀性與維護性,還能防止因不當修改導致的錯誤與資安問題。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,直到實務應用情境,完整介紹 Python 中的封裝技巧,幫助初學者到中階開發者快速上手、寫出更健全的程式碼。
核心概念
1. 為什麼需要封裝?
- 資料隱蔽:把物件內部的狀態隱藏起來,只允許透過特定方法(getter / setter)存取。
- 降低耦合:外部程式只需要知道介面,不必關心實作細節,未來若要改變內部演算法時不會影響使用者。
- 提升安全性:防止外部直接改寫關鍵屬性,避免產生不可預期的錯誤或安全漏洞。
2. Python 的存取慣例
| 修飾子 | 實際寫法 | 代表意圖 | Python 內部機制 |
|---|---|---|---|
| public | self.name |
公開,任何人皆可直接存取 | 無限制 |
| protected | _name |
受保護,建議僅在子類別或同模組內使用 | 只是一個慣例,仍可直接存取 |
| private | __name |
私有,僅限於本類別內部使用 | 會觸發 name mangling(名稱改寫) |
注意:
protected與private並不是語法強制,僅靠開發者自律與團隊規範。
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__ 之外直接修改私有屬性 |
破壞類別內部不變式 | 把所有變更邏輯集中於方法內,保持單一責任 |
最佳實踐清單
- 遵守命名慣例:單底線 → 受保護;雙底線 → 私有。團隊文件中明確列出規則。
- 盡量使用
@property:讓介面更自然,同時保留驗證與計算邏輯。 - 將驗證邏輯集中於 setter:避免在多個方法裡重複寫相同檢查。
- 保持私有屬性不可變(immutable)或僅在特定方法內變更,提升可預測性。
- 寫單元測試:測試 getter / setter 的邊界條件,確保封裝不會因未預期的輸入而失效。
實際應用場景
- 金融系統:帳戶餘額、交易紀錄等敏感資料必須以私有屬性保存,並透過嚴格的驗證介面(deposit、withdraw)控制變更。
- Web 框架的模型層:ORM(如 Django、SQLAlchemy)會把資料庫欄位封裝為屬性,使用
@property讓開發者在存取時自動觸發驗證或轉型。 - 硬體驅動程式:設備狀態(如連接狀態、緩衝區)不應被外部直接修改,透過封裝提供安全的 API。
- 大型套件的插件系統:基底類別使用雙底線隱藏內部實作,插件只能透過公開介面與之互動,避免破壞核心邏輯。
- 資料分析 pipeline:把中間結果(如 DataFrame)封裝在類別內,外部只能呼叫
get_result()取得,防止不小心改寫導致後續步驟錯誤。
總結
- 封裝 是保護物件內部狀態、提升程式可維護性的關鍵。
- Python 透過 命名慣例(單底線、雙底線)與 name mangling 來實現受保護與私有屬性。
- 使用 getter / setter 或更 Pythonic 的
@property,可以在保持簡潔語法的同時加入驗證、計算或只讀限制。 - 避免直接存取私有屬性、過度設計 getter / setter,並遵守團隊的封裝規範,才能寫出既安全又易於維護的程式碼。
掌握了封裝的正確寫法後,你的 Python 專案將更具彈性、可靠性,也更符合業界對於 乾淨程式碼(Clean Code)的期待。祝你在 OOP 的旅程中,寫出更優雅、更安全的程式!