Python的面向對象編程(OOP)中,繼承是一個強大的工具,但它也常常是初學者(甚至有經驗的開發者)的困惑之源。核心的困惑點通常圍繞:
- 為什麼子類的
__init__必須手動調用父類的__init__? super()到底是什麼,它和直接用父類名調用有何區別?
本文將通過“造房子”的類比和“鑽石問題”的深度解析,徹底澄清這些概念。
第1部分:核心問題——__init__的“覆蓋”機制
要理解為何需要 super(),首先必須理解Python的方法搜索規則。
當在子類實例上調用一個方法時(如 __init__),Python的搜索順序是:
- 先在子類中尋找該方法。
- 如果找到了,立即停止搜索並執行。
- 如果沒找到,才去父類中尋找。
這個規則會導致一個常見問題:子類 __init__ 會覆蓋父類 __init__。
類比:造房子(StandardHouse vs DeluxeHouse)
假設我們有一個“標準房子”的設計圖(父類):
class StandardHouse:
def __init__(self, foundation_type):
# 建造房子的第一步:打地基
self.foundation = foundation_type
print(f"地基已打好,類型是: {self.foundation}")
def get_info(self):
print(f"這是一座標準房子,地基是 {self.foundation}")
現在,我們想設計一個“豪華房子”(子類),它在“標準房子”的基礎上,還想額外添加一個“游泳池”。為此,我們必須定義它自己的 __init__:
錯誤的繼承(“覆蓋”導致的問題)
class DeluxeHouse(StandardHouse):
# 子類定義了自己的 __init__
def __init__(self, pool_size):
# 建造房子的第一步:建游泳池
self.pool = pool_size
print(f"游泳池已建好,大小是: {self.pool} 平米")
# --- 嘗試建造 ---
print("--- 準備建造豪華房子 ---")
my_deluxe_house = DeluxeHouse(50)
# --- 嘗試獲取信息 ---
try:
my_deluxe_house.get_info() # 調用繼承來的方法
except AttributeError as e:
print(f"\n!!! 出錯了: {e}")
執行結果:
--- 準備建造豪華房子 ---
游泳池已建好,大小是: 50 平米
!!! 出錯了: 'DeluxeHouse' object has no attribute 'foundation'
錯誤分析:
- 創建
DeluxeHouse(50)時,Python 在DeluxeHouse中找到了__init__。 - 它立刻停止搜索,並執行了子類的
__init__。 - 父類
StandardHouse的__init__從未被調用。 - 因此,
self.foundation(地基)這個屬性根本沒有被創建。 - 當
get_info()試圖訪問self.foundation時,程序崩潰。
第2部分:解決方案——super()與self的真相
DeluxeHouse 的建造者在建游泳池之前,必須顯式地(手動地)先去執行“標準房子”的建造流程(打地基)。super() 就是這個“手動調用父類”的命令。
1. super() 的含義
super():一個特殊的函數,返回一個“父類”的代理對象。super().__init__(...):通過該代理對象,調用父類的__init__方法。
2. self 的真相
從始至終,只有一個 self 對象。self 就是那個“正在被建造的實例”。
StandardHouse的建造隊(父類)和DeluxeHouse的建造隊(子類)都在同一棟房子(self)上施工。super()就是子類團隊用來“呼叫”父類團隊的電話。
正確的代碼:
class DeluxeHouse(StandardHouse):
# 子類 __init__ 必須接收 *所有* 參數(父類的 + 自己的)
def __init__(self, foundation_type, pool_size):
print("--- [子類] DeluxeHouse.__init__ 開始 ---")
# 關鍵步驟:呼叫父類團隊,先把地基打好
# 把屬於父類的參數 (foundation_type) 傳遞過去
super().__init__(foundation_type)
print("--- [子類] 父類工作已完成,現在我來添加新功能 ---")
# 2. 子類現在可以安全地設置自己的屬性
self.pool = pool_size
print(f" [子類] 游泳池已建好: {self.pool}")
print("--- [子類] DeluxeHouse.__init__ 結束 ---")
# --- 嘗試建造 ---
my_deluxe_house = DeluxeHouse("鋼筋混凝土", 50)
my_deluxe_house.get_info()
執行流程分析:
- 調用
DeluxeHouse(...),進入DeluxeHouse.__init__。 - 執行
super().__init__(...),暫停子類__init__,跳轉到父類StandardHouse.__init__。 - 父類
__init__在同一個self上設置了self.foundation。 - 父類
__init__結束,返回到子類__init__。 - 子類
__init__繼續執行,在同一個self上設置self.pool。 self對象現在同時擁有了.foundation和.pool。
第3部分:super() vs ParentClass.__init__ (新舊對比)
既然必須手動調用父類,有兩種方法可以做到:
- 現代方式 (推薦):
super().__init__(...) - 老式方式 (不推薦):
ParentClass.__init__(self, ...)
在簡單的“單繼承”場景下,兩者似乎都能工作:
class Vehicle:
def __init__(self, brand):
print(f"[Vehicle] 設置品牌為: {brand}")
self.brand = brand
# 1. 現代方式 (super)
class Car_Modern(Vehicle):
def __init__(self, brand, model):
# 語法:
# 1. 不需要寫父類名
# 2. 不需要傳遞 self
super().__init__(brand)
self.model = model
# 2. 老式方式 (父類名)
class Car_Old(Vehicle):
def __init__(self, brand, model):
# 語法:
# 1. 必須 明確寫出父類名 (Vehicle)
# 2. 必須 手動傳遞 self
Vehicle.__init__(self, brand)
self.model = model
為什麼 super() 完勝?
|
特性 |
super().__init__(...) (現代) |
ParentClass.__init__(self, ...) (老式) |
|
|
隱式傳遞。Python自動處理,代碼更簡潔。 |
必須手動傳遞。容易忘記,導致錯誤。 |
|
可維護性 |
高 (靈活) |
低 (死板 / 易碎) |
|
原因 |
如果父類改名 (如 |
如果父類改名,你必須在所有子類中搜索並替換 |
|
多重繼承 |
唯一正確的方案。 |
完全失效或導致災難性bug。 |
第4部分:決定性因素——多重繼承與“鑽石問題”
super() 存在的真正理由是:它能智能地處理複雜的多重繼承,而“父類名”調用則會徹底失敗。
經典的“鑽石問題”:D 繼承自 B 和 C,而 B 和 C 都繼承自 A。
A
/ \
B C
\ /
D
1. super() 的正確演示
super() 會智能地遵循“方法解析順序”(MRO),確保繼承鏈中的每個 __init__ 都被調用一次,且僅被調用一次。
class A:
def __init__(self):
print("A 初始化 (只應被調用一次)")
class B(A):
def __init__(self):
print("B 初始化開始...")
super().__init__()
print("B 初始化結束。")
class C(A):
def __init__(self):
print("C 初始化開始...")
super().__init__()
print("C 初始化結束。")
class D_Super(B, C): # 孫子 (使用 super)
def __init__(self):
print("D_Super 初始化開始...")
super().__init__()
print("D_Super 初始化結束。")
print("--- 測試 super() 的多重繼承 ---")
d1 = D_Super()
super() 的正確執行結果 (A只調用一次):
--- 測試 super() 的多重繼承 ---
D_Super 初始化開始...
B 初始化開始...
C 初始化開始...
A 初始化 (只應被調用一次)
C 初始化結束。
B 初始化結束。
D_Super 初始化結束。
2. “老式”方法的錯誤演示
如果 B 和 C 使用“父類名”的方式調用 A:
class B_Old(A):
def __init__(self):
print("B_Old 初始化開始...")
A.__init__(self) # 明確調用 A
print("B_Old 初始化結束。")
class C_Old(A):
def __init__(self):
print("C_Old 初始化開始...")
A.__init__(self) # 明確調用 A
print("C_Old 初始化結束。")
class D_Old(B_Old, C_Old):
def __init__(self):
print("D_Old 初始化開始...")
B_Old.__init__(self) # 明確調用 B
C_Old.__init__(self) # 明確調用 C
print("D_Old 初始化結束。")
print("\n--- 測試 '父類名' 的多重繼承 (錯誤演示) ---")
d2 = D_Old()
“老式”方法的錯誤執行結果 (A被調用兩次!):
--- 測試 '父類名' 的多重繼承 (錯誤演示) ---
D_Old 初始化開始...
B_Old 初始化開始...
A 初始化 (只應被調用一次) <-- A 被調用了第1次
B_Old 初始化結束。
C_Old 初始化開始...
A 初始化 (只應被調用一次) <-- A 被調用了第2次 (!! BUG !!)
C_Old 初始化結束。
D_Old 初始化結束。
分析:A (頂層父類) 的 __init__ 被調用了兩次!如果 A 的 __init__ 是在打開一個文件或建立一個數據庫連接,這種重複調用很可能會導致資源衝突或數據損壞。
結論與建議
|
對比項 |
super() (推薦) |
ParentClass.__init__(self, ...) (不推薦) |
|
簡單繼承 |
可用
|
可用(但不推薦)
|
|
多重繼承 |
完美工作 |
導致Bug(如重複調用) |
|
可維護性 |
高 (父類改名不影響子類) |
低 (父類改名必須重構所有子類) |
|
語法 |
簡潔 (自動處理 |
繁瑣 (必須手動傳遞 |
|
適用範圍 |
現代Python (3.x) 的標準
|
歷史遺留 (Python 2 舊代碼)
|
最終建議:
始終使用 super()。 它更簡潔、更靈活,是唯一能正確處理複雜繼承(多重繼承)的工具。ParentClass.__init__ 是一種需要被理解(以便能看懂舊代碼)但不再需要被使用的歷史寫法。