理解Python繼承之從__init__覆蓋到super()的妙用方法
Python的面向?qū)ο缶幊蹋∣OP)中,繼承是一個(gè)強(qiáng)大的工具,但它也常常是初學(xué)者(甚至有經(jīng)驗(yàn)的開發(fā)者)的困惑之源。核心的困惑點(diǎn)通常圍繞:
為什么子類的
__init__必須手動(dòng)調(diào)用父類的__init__?super()到底是什么,它和直接用父類名調(diào)用有何區(qū)別?
本文將通過“造房子”的類比和“鉆石問題”的深度解析,徹底澄清這些概念。
第1部分:核心問題——__init__的“覆蓋”機(jī)制
要理解為何需要 super(),首先必須理解Python的方法搜索規(guī)則。
當(dāng)在子類實(shí)例上調(diào)用一個(gè)方法時(shí)(如 __init__),Python的搜索順序是:
先在子類中尋找該方法。
如果找到了,立即停止搜索并執(zhí)行。
如果沒找到,才去父類中尋找。
這個(gè)規(guī)則會(huì)導(dǎo)致一個(gè)常見問題:子類 __init__ 會(huì)覆蓋父類 __init__。
類比:造房子(StandardHousevsDeluxeHouse)
假設(shè)我們有一個(gè)“標(biāo)準(zhǔn)房子”的設(shè)計(jì)圖(父類):
class StandardHouse:
def __init__(self, foundation_type):
# 建造房子的第一步:打地基
self.foundation = foundation_type
print(f"地基已打好,類型是: {self.foundation}")
def get_info(self):
print(f"這是一座標(biāo)準(zhǔn)房子,地基是 {self.foundation}")
現(xiàn)在,我們想設(shè)計(jì)一個(gè)“豪華房子”(子類),它在“標(biāo)準(zhǔn)房子”的基礎(chǔ)上,還想額外添加一個(gè)“游泳池”。為此,我們必須定義它自己的 __init__:
錯(cuò)誤的繼承(“覆蓋”導(dǎo)致的問題)
class DeluxeHouse(StandardHouse):
# 子類定義了自己的 __init__
def __init__(self, pool_size):
# 建造房子的第一步:建游泳池
self.pool = pool_size
print(f"游泳池已建好,大小是: {self.pool} 平米")
# --- 嘗試建造 ---
print("--- 準(zhǔn)備建造豪華房子 ---")
my_deluxe_house = DeluxeHouse(50)
# --- 嘗試獲取信息 ---
try:
my_deluxe_house.get_info() # 調(diào)用繼承來的方法
except AttributeError as e:
print(f"\n!!! 出錯(cuò)了: {e}")
執(zhí)行結(jié)果:
--- 準(zhǔn)備建造豪華房子 ---
游泳池已建好,大小是: 50 平米
!!! 出錯(cuò)了: 'DeluxeHouse' object has no attribute 'foundation'
錯(cuò)誤分析:
創(chuàng)建
DeluxeHouse(50)時(shí),Python 在DeluxeHouse中找到了__init__。它立刻停止搜索,并執(zhí)行了子類的
__init__。父類 StandardHouse 的 __init__ 從未被調(diào)用。
因此,
self.foundation(地基)這個(gè)屬性根本沒有被創(chuàng)建。當(dāng)
get_info()試圖訪問self.foundation時(shí),程序崩潰。
第2部分:解決方案——super()與self的真相
DeluxeHouse 的建造者在建游泳池之前,必須顯式地(手動(dòng)地)先去執(zhí)行“標(biāo)準(zhǔn)房子”的建造流程(打地基)。super() 就是這個(gè)“手動(dòng)調(diào)用父類”的命令。
1.super()的含義
super():一個(gè)特殊的函數(shù),返回一個(gè)“父類”的代理對(duì)象。super().__init__(...):通過該代理對(duì)象,調(diào)用父類的__init__方法。
2.self的真相
從始至終,只有一個(gè) self 對(duì)象。self 就是那個(gè)“正在被建造的實(shí)例”。
StandardHouse的建造隊(duì)(父類)和DeluxeHouse的建造隊(duì)(子類)都在同一棟房子(self)上施工。super()就是子類團(tuán)隊(duì)用來“呼叫”父類團(tuán)隊(duì)的電話。
正確的代碼:
class DeluxeHouse(StandardHouse):
# 子類 __init__ 必須接收 *所有* 參數(shù)(父類的 + 自己的)
def __init__(self, foundation_type, pool_size):
print("--- [子類] DeluxeHouse.__init__ 開始 ---")
# 關(guān)鍵步驟:呼叫父類團(tuán)隊(duì),先把地基打好
# 把屬于父類的參數(shù) (foundation_type) 傳遞過去
super().__init__(foundation_type)
print("--- [子類] 父類工作已完成,現(xiàn)在我來添加新功能 ---")
# 2. 子類現(xiàn)在可以安全地設(shè)置自己的屬性
self.pool = pool_size
print(f" [子類] 游泳池已建好: {self.pool}")
print("--- [子類] DeluxeHouse.__init__ 結(jié)束 ---")
# --- 嘗試建造 ---
my_deluxe_house = DeluxeHouse("鋼筋混凝土", 50)
my_deluxe_house.get_info()
執(zhí)行流程分析:
調(diào)用
DeluxeHouse(...),進(jìn)入DeluxeHouse.__init__。執(zhí)行
super().__init__(...),暫停子類__init__,跳轉(zhuǎn)到父類StandardHouse.__init__。父類
__init__在同一個(gè) self 上設(shè)置了self.foundation。父類
__init__結(jié)束,返回到子類__init__。子類
__init__繼續(xù)執(zhí)行,在同一個(gè) self 上設(shè)置self.pool。self對(duì)象現(xiàn)在同時(shí)擁有了.foundation和.pool。
第3部分:super()vsParentClass.__init__(新舊對(duì)比)
既然必須手動(dòng)調(diào)用父類,有兩種方法可以做到:
現(xiàn)代方式 (推薦):
super().__init__(...)老式方式 (不推薦):
ParentClass.__init__(self, ...)
在簡(jiǎn)單的“單繼承”場(chǎng)景下,兩者似乎都能工作:
class Vehicle:
def __init__(self, brand):
print(f"[Vehicle] 設(shè)置品牌為: {brand}")
self.brand = brand
# 1. 現(xiàn)代方式 (super)
class Car_Modern(Vehicle):
def __init__(self, brand, model):
# 語(yǔ)法:
# 1. 不需要寫父類名
# 2. 不需要傳遞 self
super().__init__(brand)
self.model = model
# 2. 老式方式 (父類名)
class Car_Old(Vehicle):
def __init__(self, brand, model):
# 語(yǔ)法:
# 1. 必須 明確寫出父類名 (Vehicle)
# 2. 必須 手動(dòng)傳遞 self
Vehicle.__init__(self, brand)
self.model = model
為什么 super() 完勝?
| 特性 | super().__init__(...) (現(xiàn)代) | ParentClass.__init__(self, ...) (老式) |
| self 參數(shù) | 隱式傳遞。Python自動(dòng)處理,代碼更簡(jiǎn)潔。 | 必須手動(dòng)傳遞。容易忘記,導(dǎo)致錯(cuò)誤。 |
| 可維護(hù)性 | 高 (靈活) | 低 (死板 / 易碎) |
| 原因 | 如果父類改名 (如 Vehicle -> BaseVehicle),子類代碼無需任何改動(dòng)。 | 如果父類改名,你必須在所有子類中搜索并替換 Vehicle.__init__。 |
| 多重繼承 | 唯一正確的方案。 | 完全失效或?qū)е聻?zāi)難性bug。 |
第4部分:決定性因素——多重繼承與“鉆石問題”
super() 存在的真正理由是:它能智能地處理復(fù)雜的多重繼承,而“父類名”調(diào)用則會(huì)徹底失敗。
經(jīng)典的“鉆石問題”:D 繼承自 B 和 C,而 B 和 C 都繼承自 A。
A
/ \
B C
\ /
D
1.super()的正確演示
super() 會(huì)智能地遵循“方法解析順序”(MRO),確保繼承鏈中的每個(gè) __init__ 都被調(diào)用一次,且僅被調(diào)用一次。
class A:
def __init__(self):
print("A 初始化 (只應(yīng)被調(diào)用一次)")
class B(A):
def __init__(self):
print("B 初始化開始...")
super().__init__()
print("B 初始化結(jié)束。")
class C(A):
def __init__(self):
print("C 初始化開始...")
super().__init__()
print("C 初始化結(jié)束。")
class D_Super(B, C): # 孫子 (使用 super)
def __init__(self):
print("D_Super 初始化開始...")
super().__init__()
print("D_Super 初始化結(jié)束。")
print("--- 測(cè)試 super() 的多重繼承 ---")
d1 = D_Super()
super() 的正確執(zhí)行結(jié)果 (A只調(diào)用一次):
--- 測(cè)試 super() 的多重繼承 ---
D_Super 初始化開始...
B 初始化開始...
C 初始化開始...
A 初始化 (只應(yīng)被調(diào)用一次)
C 初始化結(jié)束。
B 初始化結(jié)束。
D_Super 初始化結(jié)束。
2. “老式”方法的錯(cuò)誤演示
如果 B 和 C 使用“父類名”的方式調(diào)用 A:
class B_Old(A):
def __init__(self):
print("B_Old 初始化開始...")
A.__init__(self) # 明確調(diào)用 A
print("B_Old 初始化結(jié)束。")
class C_Old(A):
def __init__(self):
print("C_Old 初始化開始...")
A.__init__(self) # 明確調(diào)用 A
print("C_Old 初始化結(jié)束。")
class D_Old(B_Old, C_Old):
def __init__(self):
print("D_Old 初始化開始...")
B_Old.__init__(self) # 明確調(diào)用 B
C_Old.__init__(self) # 明確調(diào)用 C
print("D_Old 初始化結(jié)束。")
print("\n--- 測(cè)試 '父類名' 的多重繼承 (錯(cuò)誤演示) ---")
d2 = D_Old()
“老式”方法的錯(cuò)誤執(zhí)行結(jié)果 (A被調(diào)用兩次!):
--- 測(cè)試 '父類名' 的多重繼承 (錯(cuò)誤演示) ---
D_Old 初始化開始...
B_Old 初始化開始...
A 初始化 (只應(yīng)被調(diào)用一次) <-- A 被調(diào)用了第1次
B_Old 初始化結(jié)束。
C_Old 初始化開始...
A 初始化 (只應(yīng)被調(diào)用一次) <-- A 被調(diào)用了第2次 (!! BUG !!)
C_Old 初始化結(jié)束。
D_Old 初始化結(jié)束。
分析:A (頂層父類) 的 __init__ 被調(diào)用了兩次!如果 A 的 __init__ 是在打開一個(gè)文件或建立一個(gè)數(shù)據(jù)庫(kù)連接,這種重復(fù)調(diào)用很可能會(huì)導(dǎo)致資源沖突或數(shù)據(jù)損壞。
結(jié)論與建議
| 對(duì)比項(xiàng) | super() (推薦) | ParentClass.__init__(self, ...) (不推薦) |
| 簡(jiǎn)單繼承 | 可用 | 可用(但不推薦) |
| 多重繼承 | 完美工作 | 導(dǎo)致Bug(如重復(fù)調(diào)用) |
| 可維護(hù)性 | 高 (父類改名不影響子類) | 低 (父類改名必須重構(gòu)所有子類) |
| 語(yǔ)法 | 簡(jiǎn)潔 (自動(dòng)處理self) | 繁瑣 (必須手動(dòng)傳遞self) |
| 適用范圍 | 現(xiàn)代Python (3.x) 的標(biāo)準(zhǔn) | 歷史遺留 (Python 2 舊代碼) |
最終建議:
始終使用 super()。 它更簡(jiǎn)潔、更靈活,是唯一能正確處理復(fù)雜繼承(多重繼承)的工具。ParentClass.__init__ 是一種需要被理解(以便能看懂舊代碼)但不再需要被使用的歷史寫法。
到此這篇關(guān)于理解Python繼承之從__init__覆蓋到super()的妙用方法的文章就介紹到這了,更多相關(guān)Python繼承__init__覆蓋super()內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python內(nèi)置函數(shù)ord()的實(shí)現(xiàn)示例
ord()函數(shù)是用于返回字符的Unicode碼點(diǎn),適用于處理文本和國(guó)際化應(yīng)用,它只能處理單個(gè)字符,超過一字符或非字符串類型會(huì)引發(fā)TypeError,示例代碼展示了如何使用ord()進(jìn)行字符轉(zhuǎn)換和比較2024-09-09
Python?Pandas實(shí)現(xiàn)將字符串格式轉(zhuǎn)為日期時(shí)間格式
日期和時(shí)間數(shù)據(jù)在數(shù)據(jù)分析和處理中起著關(guān)鍵作用,本文將詳細(xì)介紹如何使用Pandas將字符串格式的日期時(shí)間數(shù)據(jù)轉(zhuǎn)換為日期時(shí)間格式,需要的可以參考下2024-01-01
SpringMVC和SpringBoot接收參數(shù)的幾種方式詳解
這篇文章主要介紹了SpringMVC和SpringBoot接收參數(shù)的幾種方式詳解,Spring是分層的JavaSE/EE應(yīng)用輕量級(jí)開源框架,以IoC和AOP為內(nèi)核,提供了展現(xiàn)層 Spring MVC和持久層Spring JDBC以及業(yè)務(wù)層事務(wù)管理等眾多的企業(yè)級(jí)應(yīng)用技術(shù),需要的朋友可以參考下2023-07-07
python中mechanize庫(kù)的簡(jiǎn)單使用示例
最近的項(xiàng)目中使用到了mechanize庫(kù),下面寫個(gè)簡(jiǎn)單使用的小例子給大家參考2014-01-01
Python實(shí)現(xiàn)病毒仿真器的方法示例(附demo)
這篇文章主要介紹了Python實(shí)現(xiàn)病毒仿真器的方法示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
Python實(shí)現(xiàn)解析路徑字符串并獲取每個(gè)文件夾名稱
在?Python?中,解析路徑字符串并獲取每個(gè)文件夾的名稱是一項(xiàng)常見的任務(wù),這篇文章主要為大家詳細(xì)介紹了Python解析路徑字符串的具體方法,希望對(duì)大家有所幫助2024-04-04

