Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制

1. 引言:为什么"重写"值得单独讲

在面向对象编程里,重写(Override) 是最常被使用、也最容易被误解的机制之一。很多人对它的理解停留在"子类写一个同名方法就覆盖父类"这一层,但真正写起工程代码时,问题往往出在更细的地方:

  • 为什么 super() 有时调用到了"意料之外"的父类?
  • 多继承下方法到底按什么顺序查找?
  • 重写 __init__ 后,父类的初始化逻辑为什么没执行?
  • 重写属性和重写方法有什么区别?
  • 为什么重写 __eq__ 之后对象变得不可哈希了?

本文从 Python 的对象模型出发,逐层拆解重写的底层机制:属性查找顺序 → MRO → super() 的真实语义 → 各类特殊方法的重写陷阱,最后给出工程实践中的重写规范。

2. 重写的本质:一次属性查找的结果

Python 中"方法"并不是挂在实例上的,而是挂在类 上的函数对象。调用 obj.method() 时,实际发生的是两步:

  1. 通过 type(obj).__mro__ 找到第一个定义了 method 的类;
  2. 把找到的函数绑定为方法(__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 面向对象里最容易出错的那一半。

相关推荐
摇滚侠1 小时前
《On Java 中文版 基础卷》阅读笔记 实现隐藏 12
java·数据库·笔记
西西木科技丨Shopify开发机构1 小时前
CalDigit TS4案例:AI产品图的Shopify整站接入与验收
人工智能·shopify·独立站
川石课堂软件测试2 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
2601_965448872 小时前
AI视频制作:做系列节气民俗视频音色不统一,AI 音色克隆省事的方法
人工智能·机器学习·音视频
美狐美颜SDK开放平台2 小时前
视频美颜SDK试用DEMO阶段应该测试什么?直播APP接入实操指南
开发语言·人工智能·深度学习·音视频·美颜sdk·直播美颜sdk·第三方美颜sdk
dtq04242 小时前
C++小案例 - 日志实战
开发语言·c++
HRaitest2 小时前
秋招技术岗招聘提效:三类 AI 招聘方案的技术底层与选型逻辑
大数据·人工智能·求职招聘·ai-native
天远数科2 小时前
零信任架构实战:基于天远企业股权变更构建自动化商业银行对公授信存续期股权异动与资产抽逃预警网关
运维·人工智能·架构·自动化
万化冥合2 小时前
PyBox屏幕键盘如何辅助演示键盘操作?
python·计算机外设