【Python系列课程】Python面向对象(下):封装、继承与多态

📊 阅读时长: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,核心规则是:

  1. 子类优先于父类
  2. 按照继承列表中的顺序(从左到右)
  3. 所有父类都遵循同样的规则
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 篇。关注我,不错过后续更新。

相关推荐
夕小瑶1 小时前
Claude Code 保姆级上手教程(2026 版)
人工智能·python
Lumbrologist1 小时前
【C++】零基础入门 · 第 12 节:模板与 STL 入门
开发语言·c++
天月风沙1 小时前
基于机器视觉的实验室器件仓储系统设计——内蒙古自治区国家级大创工程存档
开发语言·python
24zhgjx-fuhao1 小时前
虚链路的配置
开发语言·网络·php
weixin_468466852 小时前
机器学习之决策树新手实战指南
人工智能·python·算法·决策树·机器学习·ai
techdashen2 小时前
Rust 中的小字符串:smol_str 与 smartstring 的对决
开发语言·后端·rust
devilnumber2 小时前
java自定义事件处理器极简版:「外卖点餐」场景
java·开发语言
Hesionberger2 小时前
巧用异或找出唯一数字(多解)
java·数据结构·python·算法·leetcode
小何code2 小时前
C语言【初阶】第1节,初识C语言
c语言·开发语言