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__ 是常见的 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只暴露run和logger。WorkerInherits还暴露了log——无论这是否有意为之,调用者都能直接写worker.log(...)。解耦生命周期。 logger 可以被替换、在多个 worker 间共享,或为每个 worker 以不同方式构造,所有这些都无需触碰
Worker类。而子类则被牢牢绑死在它的基类上。避免方法名冲突。 当一个类试图同时继承两个都定义了
run的基类时,二者会冲突;而两个属性永远不会冲突。
一条实用的经验法则:
当子类确实是基类的一个种类、且基类上的每个方法对它也都讲得通时,使用继承——
Square是一种Shape;MemoryError是一种Exception。其余一切情况都使用组合——也就是当你只想要另一个类的行为、而非其身份时。大多数真实代码使用组合的频率远高于使用继承。