1. 引言:为什么"重写"值得单独讲
在面向对象编程里,重写(Override) 是最常被使用、也最容易被误解的机制之一。很多人对它的理解停留在"子类写一个同名方法就覆盖父类"这一层,但真正写起工程代码时,问题往往出在更细的地方:
- 为什么
super()有时调用到了"意料之外"的父类? - 多继承下方法到底按什么顺序查找?
- 重写
__init__后,父类的初始化逻辑为什么没执行? - 重写属性和重写方法有什么区别?
- 为什么重写
__eq__之后对象变得不可哈希了?
本文从 Python 的对象模型出发,逐层拆解重写的底层机制:属性查找顺序 → MRO → super() 的真实语义 → 各类特殊方法的重写陷阱,最后给出工程实践中的重写规范。
2. 重写的本质:一次属性查找的结果
Python 中"方法"并不是挂在实例上的,而是挂在类 上的函数对象。调用 obj.method() 时,实际发生的是两步:
- 通过
type(obj).__mro__找到第一个定义了method的类; - 把找到的函数绑定为方法(
__get__描述符协议),再调用。
所以"重写"并不是真的覆盖了什么,而是在 MRO 查找链上,子类的定义排在父类前面,因此先被找到。
python
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof"
d = Dog()
print(d.speak()) # Woof
print(Dog.speak is Animal.speak) # False,两个独立的函数对象
print(Animal.speak(d)) # ...,父类方法依然存在,只是不再被优先找到
关键结论:父类的方法从未被删除或修改 ,它只是被"遮蔽"了。这一点决定了后面所有关于 super() 的行为。
3. MRO:重写背后的查找顺序
3.1 C3 线性化
Python 3 使用 C3 线性化算法 计算 MRO(Method Resolution Order)。它保证三条性质:
- 子类永远排在父类之前;
- 多个父类按声明顺序保持相对顺序;
- 单调性:某个类的 MRO 在其所有子类的 MRO 中保持相对顺序。
python
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']
注意 A 只出现一次,且排在 B、C 之后------这就是"菱形继承"下 Python 与 C++ 虚继承不同的处理方式。
3.2 用 Mermaid 看清查找路径
#mermaid-svg-a8WdOlOGUPWTqp8O{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-a8WdOlOGUPWTqp8O .error-icon{fill:#552222;}#mermaid-svg-a8WdOlOGUPWTqp8O .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-a8WdOlOGUPWTqp8O .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-a8WdOlOGUPWTqp8O .marker{fill:#333333;stroke:#333333;}#mermaid-svg-a8WdOlOGUPWTqp8O .marker.cross{stroke:#333333;}#mermaid-svg-a8WdOlOGUPWTqp8O svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-a8WdOlOGUPWTqp8O p{margin:0;}#mermaid-svg-a8WdOlOGUPWTqp8O .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-a8WdOlOGUPWTqp8O .cluster-label text{fill:#333;}#mermaid-svg-a8WdOlOGUPWTqp8O .cluster-label span{color:#333;}#mermaid-svg-a8WdOlOGUPWTqp8O .cluster-label span p{background-color:transparent;}#mermaid-svg-a8WdOlOGUPWTqp8O .label text,#mermaid-svg-a8WdOlOGUPWTqp8O span{fill:#333;color:#333;}#mermaid-svg-a8WdOlOGUPWTqp8O .node rect,#mermaid-svg-a8WdOlOGUPWTqp8O .node circle,#mermaid-svg-a8WdOlOGUPWTqp8O .node ellipse,#mermaid-svg-a8WdOlOGUPWTqp8O .node polygon,#mermaid-svg-a8WdOlOGUPWTqp8O .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-a8WdOlOGUPWTqp8O .rough-node .label text,#mermaid-svg-a8WdOlOGUPWTqp8O .node .label text,#mermaid-svg-a8WdOlOGUPWTqp8O .image-shape .label,#mermaid-svg-a8WdOlOGUPWTqp8O .icon-shape .label{text-anchor:middle;}#mermaid-svg-a8WdOlOGUPWTqp8O .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-a8WdOlOGUPWTqp8O .rough-node .label,#mermaid-svg-a8WdOlOGUPWTqp8O .node .label,#mermaid-svg-a8WdOlOGUPWTqp8O .image-shape .label,#mermaid-svg-a8WdOlOGUPWTqp8O .icon-shape .label{text-align:center;}#mermaid-svg-a8WdOlOGUPWTqp8O .node.clickable{cursor:pointer;}#mermaid-svg-a8WdOlOGUPWTqp8O .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-a8WdOlOGUPWTqp8O .arrowheadPath{fill:#333333;}#mermaid-svg-a8WdOlOGUPWTqp8O .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-a8WdOlOGUPWTqp8O .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-a8WdOlOGUPWTqp8O .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a8WdOlOGUPWTqp8O .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-a8WdOlOGUPWTqp8O .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a8WdOlOGUPWTqp8O .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-a8WdOlOGUPWTqp8O .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-a8WdOlOGUPWTqp8O .cluster text{fill:#333;}#mermaid-svg-a8WdOlOGUPWTqp8O .cluster span{color:#333;}#mermaid-svg-a8WdOlOGUPWTqp8O div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-a8WdOlOGUPWTqp8O .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-a8WdOlOGUPWTqp8O rect.text{fill:none;stroke-width:0;}#mermaid-svg-a8WdOlOGUPWTqp8O .icon-shape,#mermaid-svg-a8WdOlOGUPWTqp8O .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a8WdOlOGUPWTqp8O .icon-shape p,#mermaid-svg-a8WdOlOGUPWTqp8O .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-a8WdOlOGUPWTqp8O .icon-shape .label rect,#mermaid-svg-a8WdOlOGUPWTqp8O .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a8WdOlOGUPWTqp8O .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-a8WdOlOGUPWTqp8O .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-a8WdOlOGUPWTqp8O :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} D(B, C)
B(A)
C(A)
A
object
MRO: D → B → C → A → object
当调用 d.speak() 时,解释器沿 D → B → C → A → object 依次查找,第一个命中的定义生效。
3.3 查看与验证 MRO
python
class A:
def who(self):
return "A"
class B(A):
def who(self):
return "B"
class C(A):
def who(self):
return "C"
class D(B, C):
pass
print(D().who()) # B
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
D 自己没有重写 who,于是按 MRO 找到 B.who。如果 B 也没有,才会轮到 C,再轮到 A。
4. super() 的真实语义
4.1 super() 不是"调用父类"
这是最常见的误解。super() 返回的是一个代理对象,它绑定两样东西:
- 一个起始类(
__class__单元格,通常是定义super()的那个类); - 一个实例或类(第一个参数)。
它的行为是:从 MRO 中"起始类之后"的位置继续查找,而不是"找父类"。
python
class A:
def hello(self):
print("A.hello")
class B(A):
def hello(self):
print("B.hello")
super().hello() # 从 B 之后继续找 → A.hello
class C(A):
def hello(self):
print("C.hello")
super().hello() # 从 C 之后继续找 → A.hello
class D(B, C):
def hello(self):
print("D.hello")
super().hello() # 从 D 之后继续找 → B.hello
D().hello()
# D.hello
# B.hello
# C.hello
# A.hello
输出顺序完美对应 MRO:D → B → C → A。这就是协作式多继承(cooperative multiple inheritance) 的核心:每个类只负责自己那一段,然后把控制权交还给 MRO 上的下一个类。
4.2 为什么必须用零参数 super()
零参数 super() 依赖编译器注入的 __class__ 隐式闭包。一旦你在嵌套函数或类外使用,就会失效:
python
class Base:
def run(self):
print("Base.run")
class Child(Base):
def run(self):
def inner():
super().run() # TypeError: super(): no arguments
inner()
正确写法是显式传参:super(Child, self).run()。这也是为什么在装饰器、闭包中重写方法时要格外小心。
4.3 super() 在类方法中的用法
python
class Base:
@classmethod
def create(cls):
return cls()
class Child(Base):
@classmethod
def create(cls):
obj = super().create() # 等价于 super(Child, cls).create()
obj.initialized = True
return obj
super() 在类方法中会自动使用 cls 作为第二个参数,无需手动传入。
5. 重写 init 的正确姿势
5.1 忘记调用父类初始化
python
class Base:
def __init__(self, name):
self.name = name
class Child(Base):
def __init__(self, name, age):
self.age = age # 忘了 super().__init__(name)
c = Child("Tom", 18)
print(c.name) # AttributeError: 'Child' object has no attribute 'name'
这是新手最常踩的坑。重写 __init__ 时,除非你明确知道父类初始化不需要,否则必须调用 super().__init__(...)。
5.2 参数透传的两种风格
python
class Base:
def __init__(self, name, **kwargs):
super().__init__(**kwargs)
self.name = name
class Child(Base):
def __init__(self, name, age, **kwargs):
super().__init__(name, **kwargs)
self.age = age
使用 **kwargs 透传是协作式多继承的标准写法:每个类只消费自己关心的参数,其余原样向上传递,避免参数签名冲突。
5.3 不要用 super() 调用非协作式父类
如果父类没有使用 **kwargs 透传,多继承时参数会断裂。此时要么改造父类,要么显式指定:Base.__init__(self, name)。后者虽然能跑,但破坏了 MRO 协作链,应尽量避免。
6. 属性重写与描述符
6.1 类属性遮蔽
python
class Base:
version = "1.0"
class Child(Base):
version = "2.0"
print(Child.version) # 2.0
print(Base.version) # 1.0
类属性重写同样是"遮蔽"语义,父类的值依然存在。
6.2 property 重写
python
class Base:
@property
def value(self):
return 1
class Child(Base):
@property
def value(self):
return super().value + 1
print(Child().value) # 2
注意 super().value 在 property 中会正确触发父类的 getter,因为 super() 代理同样实现了描述符协议。
6.3 实例属性会遮蔽类属性
python
class Base:
def get(self):
return "class method"
b = Base()
b.get = lambda: "instance attr"
print(b.get()) # instance attr
实例字典优先级高于类字典,这是 Python 属性查找的第一站。重写方法时若在实例上赋了同名属性,会直接遮蔽方法。
7. 特殊方法重写的常见陷阱
7.1 eq 与 hash 的联动
python
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
p = Point(1, 2)
print(hash(p)) # TypeError: unhashable type: 'Point'
一旦重写 __eq__,Python 会把 __hash__ 置为 None。若需要可哈希,必须显式重写:
python
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
7.2 repr 与 str
__str__ 未重写时会回退到 __repr__,反之不成立。调试场景应优先重写 __repr__,并保证它返回可重建对象的表达式。
7.3 运算符重写的返回 NotImplemented
python
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented # 不要 raise,也不要返回 None
return Money(self.amount + other.amount)
返回 NotImplemented 会让解释器尝试对方的反射方法(__radd__),这是运算符重写的正确协作方式。
8. 重写与抽象基类
python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
抽象方法必须被重写,否则实例化时抛 TypeError。这是"强制重写"的契约式表达,比文档约定可靠得多。
9. 工程实践建议
- 优先组合而非继承:重写链越深,行为越难追踪。
- 重写
__init__必调super(),并使用**kwargs透传。 - 多继承时保持协作式签名 ,所有类都接受并透传
**kwargs。 - 重写
__eq__时同步考虑__hash__。 - 运算符方法返回
NotImplemented,而不是抛异常。 - 用
__mro__排查"方法调错"问题,比读代码更快。 - 避免在实例上赋同名属性遮蔽方法。
10. 总结
Python 的重写不是"覆盖",而是 MRO 查找链上的优先级。理解这一点后:
super()的语义变得自然------它是"沿 MRO 继续往后找",而非"找父类";- 多继承的协作式写法有了理论依据;
- 各类特殊方法的陷阱(
__eq__/__hash__、NotImplemented)都能从同一套机制推导出来。
掌握 MRO 与 super(),就掌握了 Python 面向对象里最容易出错的那一半。