ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入了解Python的继承

深入了解Python的继承

前言


继承(inheritance)是 Python 里最常用的代码复用手段之一:把一组类共有的属性和方法抽到一个基类(base class,也叫父类)里,其余类通过class Child(Base)声明自己是它的子类(subclass),从而自动获得基类的一切。


很多人对继承的理解停在"子类能用父类的方法"这一层,于是遇到下面这些问题就懵了:为什么改了子类的类属性,父类也变了?为什么子类__init__忘了调super()之后父类的状态全是空的?为什么多重继承里方法调用的顺序跟写代码的顺序不一样?这些问题的答案都藏在属性查找过程和方法解析顺序(MRO)里。


本文从"继承时解释器到底做了什么"讲起,重点讲清属性查找、super()的行为,以及实际开发中最该注意的几条边界。


一、继承做了什么:属性查找的两步走


声明class Dog(Animal): ...之后,Python 会把Animal放进Dog.__bases__,并让Dog的实例在找不到某个属性时,沿着基类链继续找。具体查找分两步:



  1. 先在实例自己的__dict__里找;

  2. 找不到,再到类(及其基类)里找。类属性的查找走 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->Base

LogMixin.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 = age

2. 把可变对象写成类属性,所有实例共享同一份


❌


# 适用于 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()"。把这两个问题答清楚,大部分继承相关的怪问题都不会出现。




RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进