描述
在繪圖軟件、GIS、CAD 或簡單的圖形編輯器中,線段(Segment)是非常基礎的對象。每個線段有兩個端點(x1,y1)和(x2,y2)。在實現時我們通常希望:
- 封裝端點數據(防止外部隨意改寫造成不一致),比如修改端點后需要自動更新某些內部緩存或做驗證(不能產生零長度線段等)。
- 統計創建了多少線段(類層面的統計),但又不想讓外部隨意改這個計數(增加/減小計數會破壞統計)。
- 允許外部讀取一些信息(比如用于 UI 顯示的公開計數,或線段的標簽),同時對寫操作做控制(通過方法或屬性 setter 做驗證)。
Python 中通過“以兩個下劃線開頭但不以兩個下劃線結尾”的名字,會觸發名稱改寫(name mangling),能夠在一定程度上把屬性“隱藏”到類作用域下(并非絕對私有,但能避免偶然覆蓋/訪問)。本文以 Segment
類為例實現上述需求,并演示私有/公有屬性的典型用法與注意點。
題解答案(完整可運行代碼)
# segment_example.py
import mathclass Segment:"""表示二維平面上的一條線段(端點私有,部分類屬性私有/公有示例)"""# 私有類屬性:名稱以兩個下劃線開頭(但不以兩個下劃線結尾)__secret_count = 0# 公有類屬性:外部可以直接讀寫(但請謹慎寫)public_count = 0def __init__(self, x1=0, y1=0, x2=0, y2=0, label=None):# 私有實例屬性(用雙下劃線名字,會被 name-mangle 成 _Segment__x1 等)self.__x1 = float(x1)self.__y1 = float(y1)self.__x2 = float(x2)self.__y2 = float(y2)# 公有實例屬性(習慣上可被外部直接訪問)self.label = label# 每創建一個實例就更新統計(通過類名訪問私有類屬性)Segment.__secret_count += 1Segment.public_count += 1# 驗證:不允許零長度的線段(舉例業務規則)if self.length() == 0:raise ValueError("不允許零長度線段:兩個端點不能相同")# ---------- 公有方法:訪問/修改私有數據 -----------def set_points(self, x1, y1, x2, y2):"""設置端點(會做基本驗證)"""x1, y1, x2, y2 = float(x1), float(y1), float(x2), float(y2)if x1 == x2 and y1 == y2:raise ValueError("不允許把兩個端點設置為相同坐標(零長度)")self.__x1, self.__y1, self.__x2, self.__y2 = x1, y1, x2, y2def get_points(self):"""返回端點坐標的元組(只讀視圖)"""return (self.__x1, self.__y1, self.__x2, self.__y2)def length(self):"""返回線段長度(Euclidean distance)"""dx = self.__x2 - self.__x1dy = self.__y2 - self.__y1return math.hypot(dx, dy)def midpoint(self):"""返回線段中點坐標"""return ((self.__x1 + self.__x2) / 2.0, (self.__y1 + self.__y2) / 2.0)def translate(self, dx, dy):"""平移線段(原地修改)"""self.__x1 += dxself.__y1 += dyself.__x2 += dxself.__y2 += dy# ---------- 類方法 / 靜態方法 -----------@classmethoddef get_public_count(cls):"""返回公有計數(等價于直接訪問 cls.public_count)"""return cls.public_count@classmethoddef get_secret_count(cls):"""返回私有計數(提供受控訪問)"""return cls.__secret_countdef __repr__(self):p = self.get_points()return f"Segment(({p[0]}, {p[1]}), ({p[2]}, {p[3]}), label={self.label!r})"
題解代碼分析
下面逐個解釋代碼重點,幫助你真正理解私有與公有屬性的選擇和用法。
私有類屬性 __secret_count
__secret_count = 0
- 這是類級別的屬性。因為以兩個下劃線開頭,它會被 Python 改名(name-mangling)為
_Segment__secret_count
(內部實現),從而在外部直接用Segment.__secret_count
訪問會報錯。 - 設計初衷是:統計創建的實例數,但不希望外部隨意改寫這個統計值(雖然可以通過 name-mangling 強行訪問)。為了安全、規范,類里同時提供了公有的
public_count
(可被 UI 展示)和受控的get_secret_count()
來讀取私有值。
公有類屬性 public_count
public_count = 0
- 這是公開的類屬性,外部可直接訪問
Segment.public_count
。我們把它作為“展示用”的計數:即使外部能改它,設計上是允許展示、輕量讀寫,但關鍵邏輯仍然被私有計數保護。
私有實例屬性 __x1, __y1, __x2, __y2
self.__x1 = float(x1)
...
- 這些屬性存儲端點坐標。以雙下劃線開頭,它們在類外不會以原名出現,會被改寫成
_Segment__x1
等。 - 這樣可以降低外部代碼不小心直接賦值造成狀態不一致的可能性(例如直接把
s.__x1
改成字符串)。如果真的需要外部控制坐標,應該通過set_points()
或者用@property
/setter
做合法性檢查。
構造函數里的驗證
if self.length() == 0:raise ValueError("不允許零長度線段:兩個端點不能相同")
- 這是業務規則示例:不允許零長度線段。封裝私有數據的好處在此體現:我們能在構造/設置時統一做驗證,保證類狀態始終合法。
訪問與修改接口
get_points()
:提供只讀視圖,返回端點元組;set_points()
:提供受控修改,內部做驗證;length()
/midpoint()
/translate()
:這些都是典型的對象行為,直接在私有字段上操作,不暴露實現細節。
類方法 get_secret_count
和 get_public_count
- 即便有私有類屬性,我們仍然提供受控的讀取接口,既能保護數據,又能讓調用者獲得需要的信息。
名稱改寫(name mangling)說明
- 私有屬性并不是絕對隱藏。實際上,屬性名
__x1
在類定義內部會被解釋器改寫成_Segment__x1
。你可以從外部通過instance._Segment__x1
訪問,但這不被推薦,僅用于調試或特殊場景。 - 規則回顧:如果名字以兩個下劃線開頭但不以兩個下劃線結尾(即不是 dunder 方法),就會觸發 name mangling。像
__init__
不會被“私有化”,因為它以兩個下劃線開頭但也以兩個下劃線結尾(這是魔法方法/特殊方法)。
示例測試及結果
下面給出一系列示例用法,并展示運行結果(假定把上面類存為 segment_example.py
或直接在 REPL 執行)。
# 示例 1:創建一個正常線段
s = Segment(0, 0, 3, 4, label="A-B")
print(s) # 調用 __repr__
print("端點:", s.get_points())
print("長度:", s.length())
print("中點:", s.midpoint())
print("類的公有計數:", Segment.public_count)
print("通過類方法看公有計數:", Segment.get_public_count())
print("通過類方法看私有計數:", Segment.get_secret_count())# 示例 2:平移后驗證
s.translate(1, 1)
print("平移后端點:", s.get_points())
print("平移后長度(應保持不變):", s.length())# 示例 3:嘗試創建零長度線段(應拋異常)
try:bad = Segment(0, 0, 0, 0)
except ValueError as e:print("創建零長度線段失敗:", e)# 示例 4:演示不推薦但可行的私有屬性訪問(name-mangling)
print("私有屬性(name-mangle) x1:", s._Segment__x1)
print("私有類計數(name-mangle):", Segment._Segment__secret_count)
預期輸出(示例):
Segment((0.0, 0.0), (3.0, 4.0), label='A-B')
端點: (0.0, 0.0, 3.0, 4.0)
長度: 5.0
中點: (1.5, 2.0)
類的公有計數: 1
通過類方法看公有計數: 1
通過類方法看私有計數: 1
平移后端點: (1.0, 1.0, 4.0, 5.0)
平移后長度(應保持不變): 5.0
創建零長度線段失敗: 不允許零長度線段:兩個端點不能相同
私有屬性(name-mangle) x1: 1.0
私有類計數(name-mangle): 1
注意:最后兩個打印演示的是“可以通過 name-mangle 訪問私有數據,但這屬于越過封裝的做法,不建議在正常業務邏輯中使用”。
時間復雜度
對 Segment
中常用操作的時間復雜度分析(按單次調用計):
__init__
:O(1) —— 創建實例、賦值、做一次長度計算(常數時間)。get_points()
:O(1) —— 返回 4 元素元組。set_points()
:O(1) —— 驗證并賦值(常數時間)。length()
:O(1) —— 常數次算術運算和math.hypot
。midpoint()
:O(1) —— 常數時間。translate(dx, dy)
:O(1) —— 常數次賦值。
總體上,這個類的基本操作都是 O(1)。如果你的應用需要對大量線段做批量操作(比如 N 條線段做碰撞檢測),那整體復雜度會依據具體算法提升(例如 O(N^2) 的暴力檢測等),但這超出當前類設計范疇。
空間復雜度
單個 Segment
實例占用常數空間:保存 4 個浮點數、少量額外元數據與一個 label 引用(如果提供)。因此單個對象的空間復雜度是 O(1)。
若有 N
個 Segment
對象,總空間大致為 O(N)。
總結
- 使用雙下劃線前綴(例如
__x1
)可以觸發 Python 的 name-mangling,從而把屬性名“隱藏”在類的內部,減少外部無意的覆蓋和誤用,但并非絕對不可訪問。私有屬性用于實現數據封裝、保證內部一致性與提供受控訪問。 - 公有屬性(例如
public_count
或label
)適合用于需要頻繁讀取、用于 UI 展示或允許用戶直接定制的內容,但一旦允許寫入,就需要在設計上容忍或校驗它的變更。 - 在實際場景(繪圖工具、幾何計算、地圖標注等)中,把核心數據設為私有、對外提供受控方法是一個良好的工程實踐。這樣能把內部實現與外部接口解耦,便于以后修改實現(例如改用向量緩存、懶計算等)而不會影響外部代碼。
- 如果需要嚴格不可變的屬性,可以在外層加封裝(例如只讀 property、或使用
@dataclass(frozen=True)
的變體),但那又是另一種設計取舍。