前面我们一直把"数据"和"操作数据的函数"分开写:先定义 make_rat,再写 add_rat
但真实世界里,一个"学生"既有一堆属性(姓名、成绩),又有一堆行为(选课、算 GPA)。把它们拆在两处,代码会越来越散、越来越难找。
面向对象(OOP) 的解法很直白:把"数据"和"操作它的方法"打包进同一个类(class)。本篇从 self 这个最小齿轮讲起,一直讲到 Python 怎么决定"调的是哪个方法"(MRO)。

一、类与实例:蓝图与成品

是什么

  • 类(class) 是对象的"蓝图",描述一类事物长什么样、能干什么;
  • 实例(instance) 是按蓝图造出的具体对象;
  • self 是约定俗成的第一个参数,代表"当前这个实例自己"。
1
2
3
4
5
6
7
8
class Dog:
def __init__(self, name): # 构造器,创建时自动调用
self.name = name # 实例属性,绑在"这只狗"身上
def bark(self):
return f"{self.name}: 汪!"

d = Dog("阿黄")
print(d.bark()) # 阿黄: 汪!

输出:

1
阿黄: 汪!

__init__ 不是必须的,但最常见——它负责"出生初始化"。每次 Dog("阿黄"),Python 先造一个空实例,再把实例作为 self 传给 __init__。所以 self.name 实际上是在"这只具体的狗"身上挂了个名字。

:新手常忘记在方法里写 self. 前缀,写成裸变量 name = name,结果属性根本没存进实例,下次取就是 AttributeError凡是"属于这个实例的状态",都要 self.xxx

本质一句话:类=蓝图、实例=成品、self=当前实例;状态一律挂在 self 上才真正属于对象。

二、方法就是"绑了 self 的函数"

是什么:类里的函数叫方法(method)。调用 d.bark() 时,Python 自动把 d 作为 self 塞进第一个参数。这跟篇二"函数是对象"一脉相承——方法本质是绑在类上的函数

1
2
3
调用 d.bark() 时 Python 在背后做的:
d.bark() ──等价于─── Dog.bark(d)
↑ 自动把 d 当 self 传进去

:如果你用"函数式"写法直接拿类里的方法 f = Dog.bark,调用时得自己传 selff(d)。大多数时候用 d.bark() 让 Python 帮你传,别自找麻烦。

本质一句话:方法 = 第一个参数是 self 的函数;obj.method() 就是 Class.method(obj) 的语法糖。

三、继承:复用与特化

是什么继承(inheritance) 让子类自动获得父类的属性和方法,并可在子类里覆盖(override) 同名方法实现"特化"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."

class Cat(Animal): # Cat 继承 Animal
def speak(self):
return f"{self.name}: 喵~"

class Dog(Animal):
def speak(self):
return f"{self.name}: 汪!"

print(Cat("咪咪").speak()) # 咪咪: 喵~
print(Dog("阿黄").speak()) # 阿黄: 汪!

输出:

1
2
咪咪: 喵~
阿黄: 汪!

子类没写 __init__ 就用父类的;写了 speak覆盖父类版本。继承的价值不在"少写几行",而在"建立 is-a 的层次,统一接口"——你可以用 Animal 类型的变量装下猫和狗,统一调 speak()

:继承链过深过宽会让系统变脆——改父类一个方法,所有子类行为都可能偷偷变。业界经验法则:优先组合(has-a)而非继承(is-a)。一只"会叫的机器狗"未必是 Animal 的子类,可能只是"装了个扬声器"。

本质一句话:继承 = 子类复用父类并覆盖特化;但别滥用,组合常比继承更稳。

四、方法分派与 MRO:调的到底是哪个

是什么:当 obj.method() 被调用,Python 沿 MRO(方法解析顺序,Method Resolution Order) 查找方法:先实例自己、再当前类、再按继承顺序往上,找到第一个就停。

1
2
3
4
5
6
7
8
class A:
def hi(self): return "A"
class B(A):
def hi(self): return "B"
class C(A): pass # C 没写 hi,继承 A 的

print(B().hi()) # B(自己的)
print(C().hi()) # A(沿 MRO 找到父类)

输出:

1
2
B
A

坑(钻石继承):当继承出现"菱形"(D 同时继承 B 和 C,而 B、C 都继承 A),MRO 决定了 A 的方法只被解析一次、且顺序可预测。Python 用 C3 线性化算法保证这一点——简单记法是"按 __bases__ 顺序深度优先、去重、保序"。复杂继承树下,不画 MRO 就猜"调的是谁",基本必错。

1
2
3
4
5
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__) # 看 Python 实际解析顺序

本质一句话:方法查找走 MRO(实例→类→父类,第一个命中即停);多继承靠 C3 线性化保证顺序确定。

五、super():在子类里接住父类逻辑

是什么:覆盖方法时,常想"先跑父类的版本,再补点自己的"。super() 就是拿父类版本的钥匙,避免把父类名写死、也避免多继承时重复调用。

1
2
3
4
5
6
7
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # 调父类的 __init__ 完成 name 初始化
self.breed = breed

:在单继承super() 就是"父类";但在多继承super() 指向的是 MRO 上的"下一个类",未必是你直觉里的那个父类。所以多继承 + super() 必须配合对 MRO 的理解,否则初始化会漏掉某个基类。

小结

  • 是蓝图、实例是对象、self 指自己;状态一律挂 self
  • 方法 = 带 self 的函数;obj.m() 等价于 Class.m(obj)
  • 继承复用并覆盖特化;但优先组合、慎用深继承。
  • 方法查找走 MRO(实例→类→父类,首个命中);多继承靠 C3 线性化定序;super() 接父类逻辑。