写面向对象代码这么多年,我发现继承是最容易被滥用的语法糖。很多人学完 class Dog(Animal) 这种写法之后,就忍不住到处套用继承关系,结果代码越写越像一棵纠缠不清的家族树,改一个基类方法,四五个子类全部炸掉。今天想聊透两件事,继承和多态到底在解决什么问题,以及什么时候该收手用组合替代继承。这不是纯语法教程,更多是设计直觉的养成。
🌳 继承解决的是什么问题
继承的核心逻辑是is-a关系,也就是子类本质上是父类的一种特殊情况。狗是动物,猫是动物,它们共享动物的一些通用行为,比如呼吸、进食,但又各自有独特的叫声和习性。
来看一个最经典的写法。
python
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} is eating.")
def make_sound(self):
raise NotImplementedError("子类必须实现这个方法")
class Dog(Animal):
def make_sound(self):
print(f"{self.name} says Woof!")
class Cat(Animal):
def make_sound(self):
print(f"{self.name} says Meow!")
Dog 和 Cat 不需要重新写 eat 方法,它们从 Animal 那里白嫖了这份能力,这就是继承最直白的价值,代码复用。Real Python 的教程里对这套机制有个很好的说法,继承让你在不重写代码的情况下扩展已有类的行为 。
用一张图看看这个层级关系更直观。

继承背后藏着的坑
问题出在继承是一种极强的耦合关系 。子类和父类绑得死死的,父类的任何内部实现变化,都可能悄悄影响子类的行为,这在业界有个专门的说法叫脆弱基类问题(fragile base class problem)。GoF设计模式那本经典著作里专门用斜体强调过一句话,要优先使用对象组合,而不是类继承,这句话被印在书里加粗强调,可见分量之重 。
Reddit上一个讨论帖总结得很实在,继承确实好用,但它会在类与类之间制造出很深的依赖链条,一旦这条链条出问题,排查起来非常痛苦 。
🎭 多态不是魔法,是一种接口默契
多态这个词听起来玄乎,说穿了就是不同类型的对象,响应同一个方法调用时表现出不同的行为 。刚才 Dog 和 Cat 都实现了 make_sound,但调用方式完全一样。
python
animals = [Dog("阿黄"), Cat("小花")]
for a in animals:
a.make_sound()
调用者根本不需要知道列表里装的具体是狗还是猫,它只管调用 make_sound,这就是多态最直接的好处,写出来的代码更通用、更容易扩展新类型。
方法重写与鸭子类型是两条不同的路
多态在Python里主要靠两种方式实现。
一种是方法重写 (method overriding),子类覆盖父类的同名方法,就像上面 Dog 和 Cat 各自实现 make_sound 那样,这条路必须依赖继承关系 。
另一种是鸭子类型(duck typing),Python压根不关心对象的类型标签,只要它长得像鸭子、叫得像鸭子,就当它是鸭子。
python
class Duck:
def quack(self):
print("嘎嘎嘎")
class Person:
def quack(self):
print("我在模仿鸭子叫")
def make_it_quack(thing):
thing.quack()
make_it_quack(Duck())
make_it_quack(Person())
Duck 和 Person 之间没有任何继承关系,但只要都有 quack 方法,make_it_quack 函数就能正常工作。Stack Overflow上有个高赞回答把这两者的关系讲得很清楚,鸭子类型其实是多态的一种实现手段,多态是更宏观的设计目标,鸭子类型是Python这类动态语言里达成多态的具体方式,不依赖继承这种硬性约束 。
这也是Python和Java、C++那种静态语言思路上的一个重要分野。静态语言的多态往往需要显式的接口或抽象类,Python则更看重实际能力,不问出身。
⚖️ 组合什么时候比继承更聪明
组合的核心逻辑是has-a关系,一个对象内部持有另一个对象作为属性,而不是继承它的能力。
拿一个常见场景举例,假设要给汽车加上引擎功能。
python
class Engine:
def start(self):
print("引擎启动,轰隆隆")
class Car:
def __init__(self):
self.engine = Engine() # 组合,car拥有一个engine
def start(self):
self.engine.start()
Car 不是Engine,车不是引擎,车拥有一个引擎,这就是has-a关系。用组合而不是继承来实现这一层关系,好处是Engine和Car之间的耦合非常松散,想换成电动引擎,直接换掉self.engine指向的对象就行,Car这个类本身几乎不需要动。
The Python Coding Stack那篇专门对比两者的文章里做了个很形象的比喻,继承像是把两个类硬焊在一起,组合更像是用插头连接,想换零件的时候拔掉插头重新插一个就行,灵活度高出一个档次 。
Medium上一篇实践向的文章给出了个挺好用的判断口诀,只有当类与类之间存在清晰、逻辑上成立的is-a关系,并且确实需要共享行为时才用继承,如果只是想复用某个功能模块,而两者之间说不上是同一种东西,那就该用组合 。
两种方案摆在一起对比
| 维度 | 继承(Inheritance) | 组合(Composition) |
|---|---|---|
| 关系类型 | is-a(狗是动物) | has-a(车有引擎) |
| 耦合程度 | 紧耦合,父类改动牵连子类 | 松耦合,内部对象可替换 |
| 代码复用方式 | 子类自动拥有父类方法 | 显式调用被组合对象的方法 |
| 灵活性 | 层级固定后不易调整 | 运行时可替换内部组件 |
| 适用场景 | 类型体系天然存在层级关系 | 需要拼装不同能力、避免深层继承树 |
这张表不是说继承就不该用了,而是提醒一件事,继承树一旦画歪了,返工成本比组合高得多。python-patterns.guide那篇专门讲组合优先原则的文章里提到,业界之所以反复强调这条原则,是因为深层继承链条(所谓的God class或者钻石继承)在大型项目里会变成维护地狱,而组合天然规避了这个问题 。
🧭 一份实用的判断清单
写代码的时候纠结继承还是组合,可以问自己几个问题。

这份流程图不是死板的公式,更像是一种设计直觉的外化。真实项目里,混合使用两者才是常态,很多框架的最佳实践是用继承来定义接口契约(比如抽象基类规定必须实现哪些方法),具体功能实现则靠组合来拼装,两者不是二选一的对立关系,而是互补的工具箱。
💡 写在最后
继承和多态从来不是孤立的语法点,它们背后是一整套关于代码耦合度和可维护性的设计哲学。继承适合那些类型层级天然清晰、共享行为确实合理的场景,多态无论靠方法重写还是鸭子类型,目标都是让调用方少关心具体类型、只关心行为契约。而组合,往往是那个被低估的选项,它牺牲了一点点语法上的简洁,换来的是长期维护上的从容。
下次再想写 class Xxx(Yyy) 之前,不妨先问自己一句,Xxx真的是一种Yyy吗,还是它只是需要用到Yyy的某个功能而已。这个小小的自问自答,可能就是区分新手代码和成熟代码的分水岭。
参考资料
Python Patterns Guide - The Composition Over Inheritance Principle . python-patterns.guide/gang-of-fou...
The Python Coding Stack - Choose Your Fighter: Inheritance vs. Composition . www.thepythoncodingstack.com/p/inheritan...
Reddit r/learnprogramming - When is inheritance better than composition? . www.reddit.com/r/learnprog...
Medium - Why I Prefer Composition Over Inheritance in Python . medium.com/@philip.mut...
Real Python - Inheritance and Composition: A Python OOP Guide . realpython.com/inheritance...
GeeksforGeeks - Polymorphism in Python . www.geeksforgeeks.org/python/poly...
Codecademy - Understanding Polymorphism in Python (With Examples) . www.codecademy.com/article/und...
Stack Overflow - What is the difference between polymorphism and duck typing? . stackoverflow.com/questions/1...