继承会把父类的一切都交给子类,包括子类不该有的东西。本文从企鹅问题讲起,说明继承哪里出错,以及该用什么代替。
继承往往是大家最先想到的工具,也是最经不起时间考验的那个。原因很简单:子类会继承所有东西,包括对它并不适用的部分。
企鹅问题
# inheritance for reuse: the shape that goes wrong
class Animal:
def __init__(self, name): self.name = name
def speak(self): return '...'
def fly(self): return f"{self.name} flies away"
class Bird(Animal):
def speak(self): return f"{self.name} tweets"
class Penguin(Bird):
pass
p = Penguin('Pingu')
print(p.speak())
print(p.fly()) # inherited, and wrong
输出:
Pingu tweets
Pingu flies away
企鹅毫无疑问是鸟,这个继承层次也毫无疑问是错的。建模本身没有失误:fly 放在基类上,是因为大多数鸟会飞,写下这段代码时这话也没错。
常见的补救办法只会让事情更糟。重写 fly 让它抛异常,你就得到一个在父类接口上会失败的子类。把 fly 下移到 FlyingBird 类,那么下次遇到任何其他不会飞的东西,你又得再改一遍。
组合:能力是一个对象
# composition: the capability is an object, not an ancestor
class Wings:
def fly(self, name): return f"{name} flies away"
class Bird2:
def __init__(self, name, wings=None):
self.name, self.wings = name, wings
def speak(self): return f"{self.name} tweets"
def fly(self):
if self.wings is None:
raise TypeError(f"{self.name} cannot fly")
return self.wings.fly(self.name)
print(Bird2('Robin', Wings()).fly())
try:
Bird2('Pingu').fly()
except TypeError as err:
print(type(err).__name__ + ':', err)
输出:
Robin flies away
TypeError: Pingu cannot fly
现在,飞行是鸟拥有的东西,而不是鸟本身是的东西。加一种不会飞的鸟,既不用新建类,也不用改继承层次。
真正的好处是能替换行为
真正的理由不在于分类学,而在于组合进来的能力可以在运行时更换,也可以在测试里替换掉。
# swapping behaviour at runtime is the payoff
class JsonFormat:
def render(self, row): return f'{{"name": "{row}"}}'
class CsvFormat:
def render(self, row): return f'{row}'
class Report:
def __init__(self, fmt): self.fmt = fmt
def emit(self, rows): return [self.fmt.render(r) for r in rows]
print(Report(JsonFormat()).emit(['ada']))
print(Report(CsvFormat()).emit(['ada']))
输出:
['{"name": "ada"}']
['ada']
只有一个 Report。换成继承,就会是 JsonReport 和 CsvReport,第三种格式意味着第三个类。等哪天你想要一份两种格式兼具的报表,就真的麻烦了。
测试最能看出差别。传入一个假的格式化器,就能在完全不碰 JSON 的情况下测试 Report。用继承的话,你得写个子类才能拦截。
什么时候该用继承
继承有它的用处,只是比大家实际用它的范围窄:关系确实是“是一个”,并且基类是抽象的,也就是说它定义接口,而不是附带一堆子类未必想要的行为。
# inheritance is right when it is genuinely 'is a' and the base is abstract
class Shape:
def area(self): raise NotImplementedError
def describe(self): return f"{type(self).__name__} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r ** 2
class Square(Shape):
def __init__(self, s): self.s = s
def area(self): return self.s ** 2
for s in (Circle(1), Square(2)):
print(s.describe())
输出:
Circle with area 3.14
Square with area 4.00
Shape 承诺提供 area,并在此基础上提供 describe。没有哪个子类会继承到不该有的东西,因为根本没有具体的实现可继承。第 9 篇会讲抽象基类和 Protocol,它们能让这种承诺得到强制执行。
要点
- 继承会把一切都交给子类,包括子类不该有的东西。
- 如果你继承是为了复用代码,而不是为了声明类型,就该用组合。
- 实际的好处是能替换行为、伪造行为,而不是建模上的纯粹。
- 基类是抽象的、关系确实是“是一个”时,继承才合适。
有一条经验法则经得起真实代码的考验:如果你写子类是为了用上某个方法,你需要的是组合;如果写子类是为了承诺一个接口,继承就没问题。