2.22. 繼承¶
繼承讓一個類別得以 擴充 另一個類別 -- 先取得它所有的方法與屬性,再新增或覆寫不同之處。原始類別稱為 基底(或 父類別);擴充的類別稱為 子類別。
Square 重用 Shape 上的每個方法,並覆寫它需要特化的那些方法。¶
2.22.1. 定義子類別¶
基底類別放在 class 這一行的括號中:
class Shape:
def __init__(self, name):
self.name = name
def describe(self):
return self.name + " with area " + str(self.area())
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.describe())
輸出:
square with area 16
Square 原封不動地繼承了 Shape 的 describe,並覆寫 area 以回傳一個真正的值。基底的 describe 仍然有效,因為它呼叫的是 self.area() -- 在執行期會解析為覆寫後的版本。
2.22.2. super()¶
super() 會回傳一個代理,讓某個方法得以呼叫該方法的基底類別版本。最常見的用途是從子類別的 __init__ 呼叫基底的 __init__,好讓基底能初始化它自己的屬性:
class Square(Shape):
def __init__(self, side):
super().__init__("square") # run Shape.__init__
self.side = side
忘記 super().__init__ 是常見的錯誤來源:基底原本會設定的屬性永遠不會被設定,而依賴這些屬性的繼承方法稍後就會出錯。
2.22.2.1. 隱含的基底類別¶
每個類別都隱含繼承自 object,也就是 Python 型別階層的根。像 __repr__、__eq__ 等方法,以及屬性存取背後的機制,即使在類別未宣告任何基底時,也全都來自於此。在現代 Python 中,明確寫成 class Foo(object): 與寫成 class Foo: 是等價的;後者是慣用的形式。
2.22.3. 繼承何時有幫助 -- 何時則無¶
當一個類別確實是另一個類別 更具體的一種,並共用其大部分行為時,才使用繼承。經典的判別法是「is-a」檢驗:一個 Square 是一個 Shape。
當兩個類別只是恰好共用了少數幾個輔助方法時,繼承就是大材小用了。此時應改用 組合:讓一個類別將另一個類別的實例作為屬性持有,並透過該屬性來使用它。其形式是「has-a」,而非「is-a」:
class Logger:
def log(self, msg):
print("[log]", msg)
# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
def run(self):
self.log("starting")
# composition: Worker HAS-A Logger
class WorkerComposes:
def __init__(self):
self.logger = Logger()
def run(self):
self.logger.log("starting")
兩者都可行。組合的版本通常更好,因為它:
讓介面保持精簡。
WorkerComposes只公開run與logger。WorkerInherits還公開了log-- 呼叫者可以直接寫worker.log(...),無論這是否是原意。讓生命週期解耦。 logger 可以被替換、在多個 worker 之間共用,或為每個 worker 以不同方式建構,這一切都不必動到
Worker類別。子類別則是被牢牢綁在它的基底上。避免方法名稱衝突。 兩個都定義了
run的基底,在一個類別試圖同時繼承它們時會發生衝突;而兩個屬性則永遠不會衝突。
一個實用的經驗法則:
當子類別確實 是 基底的一種,且基底上的每個方法對它也都說得通時,才使用繼承 --
Square是一個Shape;MemoryError是一個Exception。其餘一切情況都使用組合 -- 也就是當你只是想要另一個類別的 行為,而非它的身分時。大多數實際的程式碼使用組合的頻率遠高於使用繼承。