前言
继承(inheritance)是 Python 里最常用的代码复用手段之一:把一组类共有的属性和方法抽到一个基类(base class,也叫父类)里,其余类通过class Child(Base)声明自己是它的子类(subclass),从而自动获得基类的一切。
很多人对继承的理解停在"子类能用父类的方法"这一层,于是遇到下面这些问题就懵了:为什么改了子类的类属性,父类也变了?为什么子类__init__忘了调super()之后父类的状态全是空的?为什么多重继承里方法调用的顺序跟写代码的顺序不一样?这些问题的答案都藏在属性查找过程和方法解析顺序(MRO)里。
本文从"继承时解释器到底做了什么"讲起,重点讲清属性查找、super()的行为,以及实际开发中最该注意的几条边界。
一、继承做了什么:属性查找的两步走
声明class Dog(Animal): ...之后,Python 会把Animal放进Dog.__bases__,并让Dog的实例在找不到某个属性时,沿着基类链继续找。具体查找分两步:
- 先在实例自己的
__dict__里找; - 找不到,再到类(及其基类)里找。类属性的查找走 MRO。
理解这两步,就理解了"类属性 vs 实例属性"的全部差异。
# 适用于 Python 3.8+
class Animal:
kind = "animal" # 类属性
def __init__(self, name):
self.name = name # 实例属性
class Dog(Animal):
pass
d = Dog("旺财")
print(d.name) # 旺财
print(d.kind) # animal —— 从基类 Animal 找到d.kind并不是d自己的属性,而是顺着Dog -> Animal找到的类属性。一旦执行d.kind = "dog",Python 会在实例字典里新建一个kind,从此遮蔽(shadow)掉类里的那个。
# 适用于 Python 3.8+
d.kind = "dog"
print(d.kind) # dog —— 实例属性
print(Dog.kind) # animal —— 类属性没变
print(Animal.kind) # animal二、方法解析顺序(MRO)与 C3 线性化
单继承时,属性查找就是"子类 → 父类 → 祖父类 → object",很简单。一旦有多重继承,顺序就不再直观。
Python 用C3 线性化算法计算 MRO。官方文档把它描述为:从左到右保持顺序、每个父类只调用一次、并且是单调的(子类化不会打乱已有父类的优先级)。这个顺序在类创建时就固定下来,存在__mro__里。
# 适用于 Python 3.8+
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']注意B在C前面——因为class D(B, C)里B写在左边,C3 会保持这个从左到右的次序。规则是"子类先于父类、声明靠左的先于靠右的"。
顺带纠正一个从老教程带过来的习惯:Python 2 里必须写class C(object):才能得到新式类,否则是旧式类、属性查找规则完全不同;Python 2.7 已于 2020 年 1 月 1 日停止维护,Python 3 里所有类都是新式类,class C:与class C(object):等价,括号里的object已属多余。
三、super() 与协作式继承
super()返回一个代理对象,它把方法调用委托给 MRO 中当前类之后的下一个类——注意,是"MRO 里的下一个",不是"父类"。文档里的例子说得很清楚:如果__mro__是D -> B -> C -> A -> object,而当type是B时,super()会在C -> A -> object里搜索。
这个细节是协作式多重继承的基础。看例子:
# 适用于 Python 3.8+
class Base:
def process(self):
return "Base"
class LogMixin:
def process(self):
return "Log->" + super().process()
class CacheMixin:
def process(self):
return "Cache->" + super().process()
class Service(LogMixin, CacheMixin, Base):
pass
print([c.__name__ for c in Service.__mro__])
# ['Service', 'LogMixin', 'CacheMixin', 'Base', 'object']
print(Service().process())
# Log->Cache->BaseLogMixin.process里的super()并不是指向Base,而是指向 MRO 中LogMixin之后的那一个,也就是CacheMixin。这两个 mixin 谁也没硬编码对方,super()会自动把链条串起来。只要每个参与者都调用super(),链条就完整;只要有一个环节忘了调,后面的就全部被跳过。
零参数super()有个限制:它只能在类体内部定义的普通方法里用。文档特别提醒,零参数形式在嵌套函数(包括生成器表达式,因为生成器表达式会隐式创建嵌套函数)里"不会按预期工作",因为编译器需要靠"紧邻的外层函数"来确定self。
四、继承还是组合
继承表达的是"is-a"关系,组合(composition)表达的是"has-a"。当两个类其实是"包含"而不是"属于"时,硬套继承会得到可读性差的代码。
# 适用于 Python 3.8+
# 不好:Engine 不是 Car 的一种
# class Car(Engine): ...
# 好:Car 有一个 Engine
class Engine:
def start(self):
return "引擎启动"
class Car:
def __init__(self, engine):
self.engine = engine
def start(self):
return "汽车:" + self.engine.start()常见坑点
1. 子类定义__init__后忘了调super().__init__()
❌
# 适用于 Python 3.8+
class Child(Parent):
def __init__(self, name, age):
self.name = name # 父类的初始化被完全跳过✅
# 适用于 Python 3.8+
class Child(Parent):
def __init__(self, name, age):
super().__init__(name) # 先让父类把该做的做完
self.age = age2. 把可变对象写成类属性,所有实例共享同一份
❌
# 适用于 Python 3.8+
class Team:
members = [] # 类属性,所有实例共用
def add(self, m):
self.members.append(m)Team().add("a")之后,另一个Team()的members里也有"a"。
✅
# 适用于 Python 3.8+
class Team:
def __init__(self):
self.members = [] # 每个实例各有一份3. 多重继承里直接写Base.method(self)而不用super()
❌ 硬编码某个基类,绕过 MRO,协作链断裂,某个 mixin 被静默跳过。
✅ 统一用super().method(...),让 MRO 决定顺序。
4. 覆盖方法时改了调用签名
❌ 父类def save(self, path),子类改成def save(self),多态调用一传参数就TypeError。
✅ 覆盖方法保持向后兼容的签名,新增参数一律给默认值。
5. 以为双下划线属性是真私有
❌ 子类里写self.__x,以为访问不到父类的__x,结果行为诡异。
✅ Python 没有真正的私有,__x会被名字改写(name mangling)成_ClassName__x。子类里同名属性其实改写成_Child__x,两者是不同属性,容易误解。
6. 用super()却传错参数
❌super(Child, self)在多重继承里写死成Child,一旦实例是更靠下的子类,搜索起点就错了。
✅ 方法体内直接用零参数super();只有在类体外或需要精确控制时才用两参数形式。
7. 类体里super()之外还残留isinstance判断来区分父类
❌ 明明用了继承,却仍在方法里if isinstance(self, Sub): ...,两套分发机制打架。
✅ 把差异交给方法覆盖,需要协作的部分交给super()。
8. 忘记object是所有类的最终基类
❌ 认为A.__mro__只到A为止。
✅ 每个类的 MRO 末尾都是object,这也是为什么多重继承里object一定会出现且只出现一次。
总结
| 概念 | 作用 | 关键点 |
|---|
class C(B) | 声明继承 | 写入C.__bases__ |
| 属性查找 | 找属性/方法 | 先实例__dict__,再按 MRO 找类 |
__mro__/mro() | 查看解析顺序 | C3 线性化,类创建时固定 |
super() | 调用 MRO 中的下一个类 | 不是"父类",而是"MRO 后继" |
| 组合 | has-a 关系 | 优先于继承,降低耦合 |
继承的价值在于复用和统一接口,但它不是万能胶。判断该用继承还是组合时,问一句"子类是不是真的是父类的一种";判断super()用得对不对时,问一句"我这条链条上每个人是不是都调了super()"。把这两个问题答清楚,大部分继承相关的怪问题都不会出现。