2.22. 繼承

繼承讓一個類別得以 擴充 另一個類別 -- 先取得它所有的方法與屬性,再新增或覆寫不同之處。原始類別稱為 基底(或 父類別);擴充的類別稱為 子類別

一個基底類別 Shape 向下指向子類別 Square; Square 覆寫了其中一個方法(area),並繼承其餘的方法。

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 原封不動地繼承了 Shapedescribe,並覆寫 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 只公開 runloggerWorkerInherits 還公開了 log -- 呼叫者可以直接寫 worker.log(...),無論這是否是原意。

  • 讓生命週期解耦。 logger 可以被替換、在多個 worker 之間共用,或為每個 worker 以不同方式建構,這一切都不必動到 Worker 類別。子類別則是被牢牢綁在它的基底上。

  • 避免方法名稱衝突。 兩個都定義了 run 的基底,在一個類別試圖同時繼承它們時會發生衝突;而兩個屬性則永遠不會衝突。

一個實用的經驗法則:

  • 當子類別確實 基底的一種,且基底上的每個方法對它也都說得通時,才使用繼承 -- Square 是一個 ShapeMemoryError 是一個 Exception

  • 其餘一切情況都使用組合 -- 也就是當你只是想要另一個類別的 行為,而非它的身分時。大多數實際的程式碼使用組合的頻率遠高於使用繼承。