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__ 是常见的 bug 来源:本应由基类设置的属性从未被设置,而依赖它们的继承方法稍后就会崩溃。

2.22.2.1. 隐式基类

每个类都隐式继承自 object,它是 Python 类型层次结构的根。即使一个类没有声明任何基类,诸如 __repr____eq__ 等方法以及属性访问背后的机制,全都来自那里。显式写 class Foo(object): 与写 class Foo: 在现代 Python 中是等价的;后者是惯用的写法。

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

  • 其余一切情况都使用组合——也就是当你只想要另一个类的行为、而非其身份时。大多数真实代码使用组合的频率远高于使用继承。