📊 阅读时长:24分钟 | 关键词:Python面向对象、封装、继承、多态、super()、方法重写、MRO
引言:面向对象的三大支柱
上一篇文章我们学了类和对象的基础------如何定义类、创建对象、使用属性和方法。但那只是面向对象的"语法",不是"思想"。
面向对象编程真正的威力在于三大特性:封装、继承、多态。
| 特性 | 解决的问题 | 核心机制 |
|---|---|---|
| 封装 | 如何保护数据不被随意修改? | 私有属性、私有方法 |
| 继承 | 如何复用已有代码? | 子类继承父类的属性和方法 |
| 多态 | 如何让不同对象对同一消息做出不同响应? | 方法重写、鸭子类型 |
一、封装:保护你的数据
1.1 为什么需要封装?
看一个没有封装的例子:
python
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account = BankAccount('小明', 10000)
account.balance = -50000 # 直接修改!余额变负数了!
print(account.balance) # -50000 ------ 这合理吗?
任何人都可以直接修改 balance,没有任何保护。封装就是解决这个问题的------隐藏内部实现细节,只暴露安全的接口。
1.2 Python 的访问控制
Python 没有 Java/C++ 那样的 private/public 关键字。它通过命名约定来实现访问控制:
| 命名方式 | 含义 | 外部访问 |
|---|---|---|
name |
公有属性/方法 | ✅ 可以 |
_name |
"保护"属性/方法(约定,不强制) | ⚠️ 可以但不建议 |
__name |
私有属性/方法(名称改写) | ❌ 不能直接访问 |
1.3 私有属性和方法
在属性名或方法名前加两个下划线,就变成了私有的:
python
class Person:
school = '深兰教育'
__eat = 'rice' # 私有类属性
def __init__(self, name, age):
self.name = name # 公有实例属性
self.__age = age # 私有实例属性
def get_up(self): # 公有方法
print(f'{self.name}起床了!')
def __sleep(self): # 私有方法
print(f'{self.name}睡觉了!')
# 通过公有方法访问私有属性
def get_age(self):
return self.__age
def set_age(self, age):
if 0 < age < 150:
self.__age = age
else:
print('年龄不合法!')
# 通过公有方法调用私有方法
def call_sleep(self):
self.__sleep()
p = Person('张三', 19)
# 公有属性和方法:可以直接访问
print(p.name) # '张三'
p.get_up() # 张三起床了!
# 私有属性和方法:不能直接访问
# print(p.__age) # AttributeError!
# p.__sleep() # AttributeError!
# print(Person.__eat) # AttributeError!
# 正确方式:通过公有方法间接访问
print(p.get_age()) # 19
p.call_sleep() # 张三睡觉了!
1.4 名称改写机制(Name Mangling)
Python 的"私有"不是真正的私有,而是一种名称改写:
python
class Person:
def __init__(self, name):
self.__name = name
p = Person('张三')
# p.__name # AttributeError
print(p._Person__name) # '张三' ------ 还是能访问到!
Python 把 __name 改写成了 _类名__name。这只是一个防止意外访问的机制,而不是安全机制。
Python 社区的态度:我们都是成年人,约定大于强制。如果你真的想访问私有属性,Python 不会阻止你------但你应该知道自己在做什么。
📸 图1:Python 名称改写机制图解
建议配图:左侧展示类定义中的
self.__age,右侧展示实例的__dict__中实际存储的是_Person__age。用箭头标注名称改写的规则:__属性名→_类名__属性名。标注"外部直接访问 __age 会报 AttributeError"。
1.5 封装的最佳实践
python
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # 私有属性
@property
def balance(self):
"""余额------只读属性"""
return self.__balance
def deposit(self, amount):
"""存款"""
if amount <= 0:
print('存款金额必须大于0')
return
self.__balance += amount
print(f'存款成功,当前余额:{self.__balance}')
def withdraw(self, amount):
"""取款"""
if amount <= 0:
print('取款金额必须大于0')
return
if amount > self.__balance:
print('余额不足!')
return
self.__balance -= amount
print(f'取款成功,当前余额:{self.__balance}')
# 使用
account = BankAccount('小明', 10000)
account.deposit(5000) # 存款成功,当前余额:15000
account.withdraw(2000) # 取款成功,当前余额:13000
# account.__balance = -999 # 无效!不会影响真正的余额
print(account.balance) # 13000 ------ 通过 property 安全访问
二、继承:复用代码的最佳方式
2.1 什么是继承?
继承让你基于已有的类创建新类。新类(子类)自动获得旧类(父类)的所有属性和方法。
python
# 父类(基类)
class Person:
state = 'China'
@staticmethod
def eat():
print('吃饭')
@staticmethod
def speak():
print('说话')
# 子类:继承 Person
class Student(Person):
@staticmethod
def study():
print('读书')
class Worker(Person):
@staticmethod
def work():
print('搬砖')
# 子类自动拥有父类的方法
Student.study() # 读书(自己的方法)
Student.eat() # 吃饭(继承自 Person)
Student.speak() # 说话(继承自 Person)
print(Student.state) # China(继承自 Person)
2.2 继承的查找顺序
当子类调用一个方法时,Python 按照 子类 → 父类 → 父类的父类 → ... → object 的顺序查找:
python
class Animal:
@staticmethod
def eat():
print('吃东西')
class Cat(Animal):
@staticmethod
def catch_mouse():
print('抓老鼠')
class Ragdoll(Cat):
@staticmethod
def cute():
print('卖萌')
# 继承链:Ragdoll → Cat → Animal → object
Ragdoll.cute() # 卖萌(自己的)
Ragdoll.catch_mouse() # 抓老鼠(从 Cat 继承)
Ragdoll.eat() # 吃东西(从 Animal 继承)
📸 图2:单继承链查找顺序图解
建议配图:画一个继承链:Ragdoll → Cat → Animal → object。标注方法调用时的查找顺序(从下往上),每个类标注自己的方法。用箭头标注查找方向。
2.3 多重继承
Python 支持一个类继承多个父类:
python
class Animal:
@staticmethod
def eat():
print('吃东西')
class Cat:
@staticmethod
def catch_mouse():
print('抓老鼠')
# 多重继承
class Ragdoll(Cat, Animal): # 注意顺序!
@staticmethod
def cute():
print('卖萌')
Ragdoll.cute() # 卖萌
Ragdoll.catch_mouse() # 抓老鼠
Ragdoll.eat() # 吃东西
多重继承的查找顺序遵循 MRO(Method Resolution Order,方法解析顺序):
python
print(Ragdoll.__mro__)
# (<class 'Ragdoll'>, <class 'Cat'>, <class 'Animal'>, <class 'object'>)
Python 使用 C3 线性化算法 来确定 MRO,核心规则是:
- 子类优先于父类
- 按照继承列表中的顺序(从左到右)
- 所有父类都遵循同样的规则
python
# 经典的多重继承示例
class A:
def method(self):
print('A')
class B(A):
def method(self):
print('B')
class C(A):
def method(self):
print('C')
class D(B, C): # B 在 C 前面
pass
d = D()
d.method() # 'B' ------ 先找到 B
print(D.__mro__)
# D → B → C → A → object
⚠️ 多重继承要谨慎使用。大多数情况下,单继承就足够了。多重继承会让代码变得难以理解和维护。
2.4 方法重写(Override)
子类可以重新定义父类的方法:
python
class Animal:
def __init__(self, food):
self.food = food
def eat(self):
print(f'动物吃{self.food}')
class Cat(Animal):
def eat(self): # 重写父类的 eat 方法
print(f'猫吃{self.food}') # 猫的行为不同于普通动物
c = Cat('鱼')
c.eat() # 猫吃鱼 ------ 调用了子类的 eat,不是父类的
2.5 super():调用父类的方法
重写父类方法后,如果想在子类中调用父类的版本,用 super():
python
class Animal:
def eat(self):
print('吃东西')
class Cat(Animal):
def eat(self):
print('吃鱼')
class Ragdoll(Cat):
def eat(self):
print('喝咖啡')
super().eat() # 调用父类 Cat 的 eat
super(Cat, self).eat() # 调用 Cat 的父类 Animal 的 eat
rd = Ragdoll()
rd.eat()
# 输出:
# 喝咖啡
# 吃鱼
# 吃东西
2.6 继承中的 __init__ 方法
当子类定义了 __init__,父类的 __init__ 不会自动调用:
python
class A:
def __init__(self, name):
self.name = name
print(f'A.__init__: {self.name}')
# 情况1:子类没有 __init__,自动调用父类的
class B(A):
pass
b = B('张三') # A.__init__: 张三
# 情况2:子类有 __init__,父类的不自动调用
class C(A):
def __init__(self, name):
self.name = name # 手动初始化
print(f'C.__init__: {self.name}')
c = C('赵六') # C.__init__: 赵六(A 的 __init__ 没执行)
# 情况3:子类有 __init__,但用 super() 调用父类的
class D(A):
def __init__(self, name):
super().__init__('李四') # 先调用父类的 __init__
self.name = name # 再设置子类的属性
print(f'D.__init__: {self.name}')
d = D('王五')
# A.__init__: 李四
# D.__init__: 王五
最佳实践 :如果你重写了 __init__,通常应该调用 super().__init__() 来确保父类的初始化逻辑被执行。
2.7 isinstance() 和 issubclass()
两个用于类型判断的内置函数:
python
class A:
pass
class B(A):
pass
a = A()
b = B()
# isinstance(obj, class):判断 obj 是否是 class 的实例(考虑继承)
print(isinstance(b, B)) # True
print(isinstance(b, A)) # True ------ 子类实例也是父类的实例
print(type(b) == A) # False ------ type() 不考虑继承!
# issubclass(cls, parent):判断 cls 是否是 parent 的子类
print(issubclass(B, A)) # True
print(issubclass(A, A)) # True ------ 类被视作自身的子类
print(issubclass(B, object)) # True ------ 所有类都是 object 的子类
三、多态:同一个接口,不同的行为
3.1 什么是多态?
多态的字面意思是"多种形态"------不同类的对象可以对同一个方法名做出不同的响应。
python
class Apple:
@staticmethod
def change():
return '啊~ 我变成了苹果汁!'
class Banana:
@staticmethod
def change():
return '啊~ 我变成了香蕉汁!'
class Mango:
@staticmethod
def change():
return '啊~ 我变成了芒果汁!'
class Juicer:
@staticmethod
def work(fruit):
"""榨汁机:只要 fruit 有 change() 方法,就能工作"""
print(fruit.change())
# 不同水果,同样的 change() 方法,不同的结果
Juicer.work(Apple()) # 啊~ 我变成了苹果汁!
Juicer.work(Banana()) # 啊~ 我变成了香蕉汁!
Juicer.work(Mango()) # 啊~ 我变成了芒果汁!
多态的核心 :Juicer.work() 不关心传入的是什么类型,只关心它有没有 change() 方法。这就是 Python 著名的 "鸭子类型"(Duck Typing):
如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。
3.2 鸭子类型的实际应用
python
# 所有有 draw() 方法的对象都可以传入
class Circle:
def draw(self):
print('画一个圆 ○')
class Square:
def draw(self):
print('画一个方框 □')
class Triangle:
def draw(self):
print('画一个三角形 △')
def render(shape):
"""渲染图形------不关心类型,只关心有没有 draw() 方法"""
shape.draw()
render(Circle()) # 画一个圆 ○
render(Square()) # 画一个方框 □
render(Triangle()) # 画一个三角形 △
这就是 Python 多态的精髓------不依赖继承关系,只依赖对象的行为。
四、面向对象综合示例:学生和老师的一天
让我们把学到的封装、继承、多态结合起来,完成一个完整的例子:
python
class Person:
"""人类------基类"""
def __init__(self, name, age):
self.name = name
self.age = age
self.show_info() # 创建时自动介绍自己
def get_up(self):
print(f'{self.name}睁开眼睛 → 起身 → 穿好衣服')
def wash(self):
print(f'{self.name}刷牙 → 洗脸')
def eat(self):
print(f'{self.name}吃菜 → 扒饭')
def sleep(self):
print(f'{self.name}脱掉外套 → 躺下 → 闭上眼睛')
def show_info(self):
pass # 由子类实现
class Student(Person):
"""学生类"""
count = 0 # 类属性:统计学生人数
def __init__(self, name, age, grade):
self.grade = grade
super().__init__(name, age) # 调用父类 __init__
Student.count += 1
def show_info(self):
print(f'大家好!我是{self.name},今年{self.age}岁,在读{self.grade}!')
def login(self):
print(f'{self.name}输入账号密码 → 登录成功')
def study(self):
print(f'{self.name}看视频 → 查资料 → 写代码')
@classmethod
def publish(cls):
print(f'当前学生人数:{cls.count}')
class Teacher(Person):
"""老师类"""
count = 0
def __init__(self, name, age, department):
self.department = department
super().__init__(name, age)
Teacher.count += 1
def show_info(self):
print(f'大家好!我是{self.name},今年{self.age}岁,在{self.department}任职!')
def clock_in(self):
print(f'{self.name}录入指纹 → 打卡成功')
def work(self):
print(f'{self.name}授课 → 答疑 → 写代码')
@classmethod
def publish(cls):
print(f'当前老师人数:{cls.count}')
# 模拟一天
def simulate_day(person):
"""多态:不管是学生还是老师,都能执行一天的活动"""
person.get_up()
person.wash()
person.eat()
# 不同角色的特殊行为
if isinstance(person, Student):
person.login()
person.study()
elif isinstance(person, Teacher):
person.clock_in()
person.work()
person.eat()
if isinstance(person, Student):
person.study()
elif isinstance(person, Teacher):
person.work()
person.eat()
person.wash()
person.sleep()
# 使用
stu1 = Student('张三', 18, '高三')
stu2 = Student('李四', 16, '高一')
t1 = Teacher('老赵', 39, '教学部')
Student.publish() # 当前学生人数:2
Teacher.publish() # 当前老师人数:1
print('\n===== 张三的一天 =====')
simulate_day(stu1)
print('\n===== 老赵的一天 =====')
simulate_day(t1)
📸 图3:Person-Student-Teacher 继承层次结构图
建议配图:用 UML 类图展示 Person(基类)→ Student 和 Teacher(子类)的继承关系。标注每个类的属性(name、age、grade、department)和方法(get_up、wash、eat、sleep、show_info、study、work 等)。用不同颜色区分继承的方法和子类特有的方法。
五、动手练习
练习 1:实现一个简单的形状类层次结构
python
import math
class Shape:
"""形状基类"""
def area(self):
raise NotImplementedError('子类必须实现 area 方法')
def perimeter(self):
raise NotImplementedError('子类必须实现 perimeter 方法')
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# 多态:同样的接口,不同的计算
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]
for shape in shapes:
print(f'面积:{shape.area():.2f},周长:{shape.perimeter():.2f}')
练习 2:银行账户的继承
python
# 在 BankAccount 基础上,实现以下子类:
# SavingsAccount:有年利率,可以计算利息
# CreditAccount:有信用额度,取款不能超过 余额+额度
小结
这篇文章覆盖了面向对象编程的三大特性:
| 特性 | 核心机制 | 关键语法 |
|---|---|---|
| 封装 | 隐藏实现细节,暴露安全接口 | __属性名(名称改写为 _类名__属性名) |
| 继承 | 子类复用父类的代码 | class 子类(父类):、super()、MRO |
| 多态 | 不同对象对同一方法做出不同响应 | 方法重写 + 鸭子类型 |
下一篇文章,我们将进入面向对象的进阶话题------魔术方法、__str__/__repr__、运算符重载、上下文管理器------这些是让你写出 Pythonic 代码的关键。
本文是「Python从入门到数据分析」系列的第 10 篇,共 24 篇。关注我,不错过后续更新。