一、為什麼 Python 需要屬性封裝?
在前一篇文章中,我們認識了 Python 物件導向程式設計(OOP)的封裝(Encapsulation),以及三種常見的屬性命名方式:
self.oxygen:公開屬性self._oxygen:非公開慣例self.__oxygen:名稱改寫
但為什麼需要這三種寫法?它們真的能保護資料嗎?
我們從智慧養殖池的溶氧監控系統開始。
生活情境:養殖池的溶氧數值
假設我們正在開發一套智慧養殖系統,每個養殖池都有一個溶氧值(DO,Dissolved Oxygen),單位是 mg/L。
溶氧值通常不應該是負數。然而,如果我們直接使用公開屬性,外部程式就可能寫入不合理的資料。
class FishPond:
def __init__(self, oxygen):
self.oxygen = oxygen
pond = FishPond(6.5)
print(pond.oxygen)
pond.oxygen = -10
print(pond.oxygen)
執行結果:
6.5
-10
程式可以正常執行,但溶氧值卻變成 -10 mg/L。
問題不在 Python 程式發生錯誤,而是程式允許外部直接寫入不合理的數值。
這就是封裝希望解決的問題之一。
二、第一種寫法:公開屬性 self.oxygen
公開屬性(Public Attribute)是 Python 最直接的屬性寫法。
class FishPond:
def __init__(self, oxygen):
self.oxygen = oxygen
pond = FishPond(6.5)
print(pond.oxygen)
pond.oxygen = 7.2
print(pond.oxygen)
執行結果:
6.5
7.2
這種寫法非常簡單,外部可以直接讀取,也可以直接修改。
適合情境:當屬性沒有特殊的驗證需求,而且允許使用者自由修改時,公開屬性通常是最簡單、最符合 Python 風格的選擇。
但如果某個屬性具有特定規則,例如溶氧不能小於零,就需要進一步考慮如何控制修改行為。
三、第二種寫法:單底線 self._oxygen
在 Python 中,屬性名稱前面加上一個底線,通常表示:
「這個屬性主要供類別內部使用,外部程式不應隨意依賴或修改。」
class FishPond:
def __init__(self, oxygen):
self._oxygen = oxygen
def show_oxygen(self):
print("目前溶氧:", self._oxygen)
pond = FishPond(6.5)
pond.show_oxygen()
pond._oxygen = -10
pond.show_oxygen()
執行結果:
目前溶氧: 6.5
目前溶氧: -10
我們發現,雖然使用了單底線,外部仍然可以修改這個屬性。
原因是:單底線是一種命名慣例,不是 Python 強制執行的存取限制。
在某些物件導向語言中,會使用 protected 描述受保護的成員。但 Python 的單底線並不等同於 Java、C++ 等語言的存取控制機制。
可以把單底線想像成養殖池控制箱上的標籤:
內部設定,請勿任意修改。
這個標籤提醒使用者遵守規範,但並沒有真的把控制箱鎖起來。
四、第三種寫法:雙底線 self.__oxygen
如果把屬性名稱改成雙底線開頭,會發生什麼事情?
class FishPond:
def __init__(self, oxygen):
self.__oxygen = oxygen
def show_oxygen(self):
print("目前溶氧:", self.__oxygen)
pond = FishPond(6.5)
pond.show_oxygen()
print(pond.__oxygen)
執行結果:
目前溶氧: 6.5
AttributeError: 'FishPond' object
has no attribute '__oxygen'
為什麼類別內部可以使用 self.__oxygen,但是外部卻不能直接使用 pond.__oxygen?
這是因為 Python 有一項特殊機制:
名稱改寫(Name Mangling)。
五、深入理解 Name Mangling 名稱改寫
當 Python 在類別定義中遇到雙底線開頭,而且不是雙底線結尾的屬性名稱時,會依照類別名稱進行名稱改寫。
例如:
class FishPond:
def __init__(self, oxygen):
self.__oxygen = oxygen
Python 會將類別中的 __oxygen 改寫為:
__oxygen
↓
_FishPond__oxygen
注意:這不是加密,也不是把資料變成無法存取。
我們可以使用 __dict__ 觀察一般實例的屬性儲存內容。
class FishPond:
def __init__(self, oxygen):
self.__oxygen = oxygen
pond = FishPond(6.5)
print(pond.__dict__)
執行結果:
{'_FishPond__oxygen': 6.5}
現在我們知道,屬性其實仍然存在,只是名稱被改寫了。
名稱改寫真的不能從外部存取嗎?
答案是:仍然可以。
pond = FishPond(6.5)
print(pond._FishPond__oxygen)
pond._FishPond__oxygen = 8.0
print(pond._FishPond__oxygen)
執行結果:
6.5
8.0
因此,雙底線不是安全機制,也不能用來防止惡意修改資料。
它的主要目的,是避免意外的名稱衝突,特別是在繼承的情境中。
六、為什麼名稱改寫可以避免繼承時的衝突?
假設我們有一個基本養殖池類別 FishPond,以及一個智慧養殖池類別 SmartFishPond。
父類別與子類別都使用 __oxygen,會不會互相覆蓋?
class FishPond:
def __init__(self):
self.__oxygen = 6.5
def show_parent(self):
print("父類別溶氧:", self.__oxygen)
class SmartFishPond(FishPond):
def __init__(self):
super().__init__()
self.__oxygen = 8.0
def show_child(self):
print("子類別溶氧:", self.__oxygen)
pond = SmartFishPond()
pond.show_parent()
pond.show_child()
print(pond.__dict__)
執行結果:
父類別溶氧: 6.5
子類別溶氧: 8.0
{'_FishPond__oxygen': 6.5,
'_SmartFishPond__oxygen': 8.0}
雖然父類別與子類別都使用 __oxygen,但 Python 會根據各自定義所在的類別進行名稱改寫。
| 類別 | 改寫後的名稱 |
|---|---|
| FishPond | _FishPond__oxygen |
| SmartFishPond | _SmartFishPond__oxygen |
因此兩個屬性可以同時存在。
不過,這個範例只是示範名稱改寫。實際開發智慧養殖系統時,如果父類別和子類別需要共用同一個溶氧數值,通常應設計共同的屬性介面,而不是各自儲存一份不同的溶氧資料。
七、真正重要的封裝:控制資料如何被修改
到目前為止,我們發現三種屬性命名方式,都不能真正阻止外部程式修改資料。
那麼,封裝到底有什麼價值?
封裝的重點不只是隱藏資料,而是:
將資料與操作資料的規則放在同一個類別中,提供一致、可維護的操作介面。
例如,智慧養殖池的溶氧值必須符合以下條件:
- 必須是數值。
- 不能小於 0 mg/L。
- 可以透過類別提供的方法更新。
先使用一般方法實作。
class FishPond:
def __init__(self, oxygen):
self.__oxygen = 0
self.set_oxygen(oxygen)
def set_oxygen(self, value):
if isinstance(value, bool) or not isinstance(
value, (int, float)
):
print("錯誤:溶氧必須是數值")
elif not 0 <= value < float("inf"):
print("錯誤:溶氧必須是有限的非負數")
else:
self.__oxygen = value
def get_oxygen(self):
return self.__oxygen
pond = FishPond(6.5)
print(pond.get_oxygen())
pond.set_oxygen(-10)
print(pond.get_oxygen())
執行結果:
6.5
錯誤:溶氧必須是有限的非負數
6.5
當外部程式嘗試設定 -10 時,類別會拒絕這個不合理的數值,並保留原本的 6.5。
這就是封裝帶來的好處:由類別統一管理資料修改規則。
但要注意,這個規則只會在使用者透過 set_oxygen() 修改資料時發揮作用。它不是防止所有外部修改的安全邊界。
八、Python 更推薦的方式:@property
在其他物件導向語言中,我們經常看到 Getter 和 Setter 方法。
Python 也可以使用這種方式,但還提供了更自然的設計:
@property 裝飾器。
它讓我們可以使用類似公開屬性的寫法,同時執行讀取與設定時的控制邏輯。
class FishPond:
def __init__(self, oxygen):
self._oxygen = 0
self.oxygen = oxygen
@property
def oxygen(self):
return self._oxygen
@oxygen.setter
def oxygen(self, value):
if isinstance(value, bool) or not isinstance(
value, (int, float)
):
raise TypeError("溶氧必須是數值")
if not 0 <= value < float("inf"):
raise ValueError("溶氧必須是有限的非負數")
self._oxygen = value
pond = FishPond(6.5)
print(pond.oxygen)
pond.oxygen = 7.2
print(pond.oxygen)
執行結果:
6.5
7.2
這裡有兩個重要的裝飾器:
| 裝飾器 | 功能 |
|---|---|
@property |
定義讀取屬性時執行的方法 |
@oxygen.setter |
定義設定屬性時執行的方法 |
例如:
print(pond.oxygen)
會透過 Getter 讀取數值。
pond.oxygen = 7.2
會透過 Setter 檢查並更新數值。
使用者不需要直接呼叫 get_oxygen() 或 set_oxygen()。
另外,這裡使用 raise 主動拋出例外,當輸入資料不符合規則時,程式不會默默接受錯誤數值。
若要讓程式在錯誤輸入後繼續執行,可以在呼叫端搭配 try...except 處理。
九、整合實作:智慧養殖池溶氧監控系統
現在我們把屬性封裝與智慧養殖的實際需求結合。
這個範例具備以下功能:
- 建立具有名稱的養殖池。
- 設定與讀取溶氧數值。
- 檢查溶氧是否合理。
- 根據溶氧數值顯示監控狀態。
以下設定 5.0 mg/L 為教學示範警戒值,實際養殖時應依魚種、環境及養殖管理需求調整。
class SmartFishPond:
def __init__(self, name, oxygen):
self.name = name
self.__oxygen = 0
self.oxygen = oxygen
@property
def oxygen(self):
return self.__oxygen
@oxygen.setter
def oxygen(self, value):
if isinstance(value, bool) or not isinstance(
value, (int, float)
):
raise TypeError("溶氧必須是數值")
if not 0 <= value < float("inf"):
raise ValueError("溶氧必須是有限的非負數")
self.__oxygen = value
def check_status(self):
if self.oxygen < 5.0:
return "溶氧偏低,請檢查設備"
else:
return "溶氧達到示範標準"
def show_status(self):
print("養殖池:", self.name)
print("溶氧:", self.oxygen, "mg/L")
print("狀態:", self.check_status())
pond = SmartFishPond("水井村一號池", 6.5)
pond.show_status()
print("----- 更新感測數據 -----")
pond.oxygen = 3.8
pond.show_status()
執行結果:
養殖池: 水井村一號池
溶氧: 6.5 mg/L
狀態: 溶氧達到示範標準
----- 更新感測數據 -----
養殖池: 水井村一號池
溶氧: 3.8 mg/L
狀態: 溶氧偏低,請檢查設備
在這個類別中,我們同時使用了三種概念:
self.name:公開養殖池名稱。self.__oxygen:以名稱改寫降低意外名稱衝突的風險。@property:提供一致的溶氧讀取與更新介面,並驗證數值。
這個範例仍屬教學用程式,並未直接連接感測器或控制增氧設備。未來可以結合 ESP32、Raspberry Pi Pico W、MQTT 或雲端監控平台,擴充為實際的智慧養殖應用。
十、三種屬性命名與 @property 比較
| 寫法 | 主要用途 | 是否阻止外部存取 |
|---|---|---|
self.oxygen |
公開屬性 | 否 |
self._oxygen |
內部使用慣例 | 否 |
self.__oxygen |
名稱改寫、降低命名衝突 | 不允許以原名稱直接存取,但仍可用改寫後名稱存取 |
@property |
控制屬性讀取與設定行為 | 可控制正常介面的讀寫,但不是安全隔離機制 |
十一、課堂思考與延伸練習
練習一:屬性命名
請將智慧養殖池中的 oxygen 分別改為 _oxygen 與 __oxygen,觀察外部程式存取時的差異。
練習二:名稱改寫
使用 __dict__ 印出物件的屬性,觀察 Python 如何改寫雙底線屬性名稱。
練習三:資料驗證
修改 @property 的 Setter,讓溶氧值必須介於 0~20 mg/L。這是程式驗證練習範圍,不代表所有養殖環境的實際量測範圍。
練習四:智慧養殖擴充
新增 temperature 水溫屬性,並使用 @property 驗證輸入是否為合理數值。
進階挑戰:設計一個 SmartFishPond 類別,同時管理溶氧、水溫與水位,並利用方法輸出完整的養殖池狀態。
十二、結語:封裝不只是把資料藏起來
學習 Python 物件導向程式設計時,很容易誤以為封裝就是把屬性改成雙底線。
但真正重要的觀念是:
單底線,提醒哪些資料屬於內部實作。
雙底線,降低繼承時意外命名衝突的風險。
@property,讓資料操作遵守類別定義的規則。
因此,Python 的封裝不是單純追求「不讓別人看到資料」,而是透過良好的類別設計,讓資料與操作行為保持一致。
從智慧養殖池的溶氧監控開始,我們可以進一步整合繼承、多型、例外處理與物聯網感測技術,逐步建立完整的智慧生活應用系統。