Blog

Python 中的组合优于继承

继承会把父类的一切都交给子类,包括子类不该有的东西。本文从企鹅问题讲起,说明继承哪里出错,以及该用什么代替。

继承往往是大家最先想到的工具,也是最经不起时间考验的那个。原因很简单:子类会继承所有东西,包括对它并不适用的部分。

企鹅问题

# 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。换成继承,就会是 JsonReportCsvReport,第三种格式意味着第三个类。等哪天你想要一份两种格式兼具的报表,就真的麻烦了。

测试最能看出差别。传入一个假的格式化器,就能在完全不碰 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,它们能让这种承诺得到强制执行。

要点

  • 继承会把一切都交给子类,包括子类不该有的东西。
  • 如果你继承是为了复用代码,而不是为了声明类型,就该用组合。
  • 实际的好处是能替换行为、伪造行为,而不是建模上的纯粹。
  • 基类是抽象的、关系确实是“是一个”时,继承才合适。

有一条经验法则经得起真实代码的考验:如果你写子类是为了用上某个方法,你需要的是组合;如果写子类是为了承诺一个接口,继承就没问题。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。