Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
- Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
- Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
- Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
- Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
- Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
- Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
- Python 系列(二十六) Python 模块入门,从 import 到模块复用
- Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
- Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
- Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
- Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
- Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
- Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
- Python 系列(三十三) 什么是类和对象?零基础理解 Python class
- Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
- Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
- Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
- Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
- Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
- Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
- Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
- Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
前面我们学习了 Python 的类和对象,知道可以通过类描述一类事物的共同特征,再创建多个对象来表示不同的实际事物。
假设现在需要管理 Linux 服务器,既有 Web 服务器,也有数据库服务器,还有缓存服务器。它们都有服务器名称、IP 地址等共同信息,但提供的服务又各不相同。
如果每种服务器都从头编写一遍代码,不仅重复工作多,后期维护也很麻烦。
有没有办法让不同类型的服务器复用共同的代码,同时保留各自的特点?
这就是今天要学习的两个概念:继承与多态。
从重复代码说起
先来看一个实际问题。
我们需要管理一台 Web 服务器和一台数据库服务器。两种服务器都有名称和 IP 地址,因此可以分别定义两个类。
创建一个名为 server_demo.py 的文件,写入下面的代码:
1 | class WebServer: |
这里先不急着记忆每一行代码,我们先关注两个类的共同点。
它们都使用 __init__() 保存服务器名称和 IP 地址,也都提供了 start() 方法。区别只是启动时输出的文字不同。
运行程序:
1 | python server_demo.py |
输出结果:
1 | Web 服务器 web01 已启动,IP:192.168.1.10 |
程序虽然可以正常运行,但两个类中存在重复代码。
如果以后还要增加缓存服务器、文件服务器、消息队列服务器,我们可能需要反复编写保存名称和 IP 地址的代码。
能不能把这些共同的内容集中起来,让其他类直接使用?
当然可以。这就需要学习继承。
理解继承:让子类复用父类的代码
在现实生活中,Web 服务器和数据库服务器虽然用途不同,但它们都属于服务器。
我们可以把这种关系理解为:
- 父类:描述多个子类共同拥有的属性和方法。
- 子类:在父类的基础上,继承已有的属性和方法,还可以增加自己的功能或重新定义某些行为。
Python 中的继承写法很简单:定义子类时,在类名后面的括号中写上父类的名称。
先把 server_demo.py 修改为下面的内容:
1 | class Server: |
运行程序:
1 | python server_demo.py |
输出结果:
1 | web01 |
第一次看到这里,可能会有一个疑问:WebServer 类明明没有定义 __init__(),为什么创建对象时还可以传入名称和 IP 地址?
关键就在这里:
1 | class WebServer(Server): |
WebServer(Server) 表示 WebServer 继承了 Server。
因此,WebServer 可以使用父类 Server 中定义的 __init__() 方法。创建 WebServer 对象时,Python 会调用这个继承而来的方法,将名称和 IP 地址保存到对象中。
这里的 pass 是 Python 的占位语句,表示暂时不执行任何操作。它让我们可以先定义一个空的类,而不必马上编写方法或属性。
这段代码说明:子类即使没有重新定义初始化方法,也可以使用从父类继承来的方法。
接下来,我们不只是观察程序能否运行,还要验证继承关系是否真的存在。
动手验证:子类真的继承了父类吗
程序输出了名称和 IP 地址,只能说明对象可以正常使用这些属性。我们还可以直接检查类之间的关系。
在 server_demo.py 中保留刚才的 Server 和 WebServer 定义,并把最后几行改成:
1 | web01 = WebServer("web01", "192.168.1.10") |
运行程序:
1 | python server_demo.py |
输出结果:
1 | True |
这四个结果分别是什么意思?
先看第一行:
1 | print(isinstance(web01, WebServer)) |
isinstance() 用来判断一个对象是否属于指定的类,或者是否属于这个类的子类。
web01 是通过 WebServer 创建的对象,所以结果是 True,表示判断成立。
再看第二行:
1 | print(isinstance(web01, Server)) |
虽然 web01 是通过 WebServer 创建的,但 WebServer 继承了 Server。因此,web01 不仅是一个 WebServer 对象,也被视为一个 Server 类型的对象。
所以结果仍然是 True。
第三行:
1 | print(issubclass(WebServer, Server)) |
issubclass() 用来判断一个类是否是另一个类的子类。
这里是在判断 WebServer 是否继承了 Server。答案是肯定的,因此结果为 True。
最后一行:
1 | print(issubclass(Server, WebServer)) |
这次把父类和子类的位置交换了。
Server 是父类,WebServer 是子类,父类并不是子类的子类,所以结果为 False。
把这几个判断整理一下:
| 判断代码 | 检查的内容 | 结果 |
|---|---|---|
isinstance(web01, WebServer) | 对象是否属于 WebServer | True |
isinstance(web01, Server) | 对象是否属于 Server 或其子类 | True |
issubclass(WebServer, Server) | WebServer 是否是 Server 的子类 | True |
issubclass(Server, WebServer) | Server 是否是 WebServer 的子类 | False |
这里先记住一个实用区别:isinstance() 检查对象,issubclass() 检查类。
下一篇会专门学习这些内置函数,包括如何判断对象的类型,以及如何判断两个类之间的继承关系。
空类继承有什么实际意义
现在我们已经看到,下面的代码即使没有定义任何方法,也能正常工作:
1 | class WebServer(Server): |
那么问题来了:既然 WebServer 什么都没写,为什么还要定义它?直接使用 Server 不就行了吗?
这个问题非常重要。
空类继承并不是为了少写一行代码,而是为了先明确对象的类别,同时复用父类的功能,为以后扩展留下空间。
我们用一个更贴近运维工作的例子来理解。
假设公司有多台服务器,其中既有普通服务器,也有 Web 服务器。现在暂时不需要给 Web 服务器增加专属方法,但我们希望程序能够区分它们。
可以这样定义:
1 | class Server: |
运行结果:
1 | server01 False |
注意,这里没有给 WebServer 增加任何新方法,但程序已经能够区分普通服务器和 Web 服务器。
这是因为两者虽然都具备父类提供的名称和 IP 地址,但它们属于不同的类。
在实际项目中,这种区别可以用于分类统计、选择不同的处理流程,或者为后续扩展提供基础。例如,以后可以只对 Web 服务器执行网站服务检查,而对数据库服务器执行数据库服务检查。
当然,如果一个类既没有独立的业务含义,也不需要特殊处理或未来扩展,就没有必要为了使用继承而刻意创建空子类。 是否创建子类,应该由实际需求决定。
让子类拥有自己的功能
前面我们学习了如何通过继承复用父类的初始化方法。
但是,不同类型的服务器不仅名称和 IP 地址不同,执行的操作也可能不同。
例如,Web 服务器提供网站服务,数据库服务器负责数据库服务,缓存服务器负责缓存服务。
我们可以先在父类中定义一个通用的 start() 方法,再让不同的子类根据自己的需要重新定义它。
修改 server_demo.py:
1 | class Server: |
运行结果:
1 | Web 服务器 web01 已启动,IP:192.168.1.10 |
这里有一个新概念:方法重写。
方法就是定义在类中的函数。例如,前面定义的 start() 就是一个方法。
当子类重新定义了一个与父类同名的方法时,就可以改变该子类调用这个方法时的行为。这种做法叫作方法重写。
在上面的例子中:
Server定义了通用的start()方法。WebServer重写了start(),输出 Web 服务器的信息。DatabaseServer重写了start(),输出数据库服务器的信息。
如果子类没有重写某个方法,就可以直接使用父类继承而来的实现。
需要注意的是,这里展示的是程序输出信息,并没有真正启动 Linux 服务。真实的服务管理还需要调用操作系统命令或相应的 API。
理解多态:同一个方法,不同的表现
现在,我们已经知道不同的子类可以拥有自己的 start() 方法。
那么,能不能把不同类型的服务器放到同一个列表中,然后用同一段代码依次调用它们的启动方法?
可以。这就是多态最直观的应用。
先看完整示例:
1 | class Server: |
运行结果:
1 | Web 服务器 web01 已启动,IP:192.168.1.10 |
我们一步一步分析这段代码。
首先,servers 是一个列表,用来保存三个不同类型的服务器对象。
其次,for server in servers 表示依次取出列表中的每个对象,并使用 server 这个名称引用当前对象。
最后,程序每次执行:
1 | server.start() |
调用的都是 start(),但具体执行哪个版本的方法,取决于当前对象实际所属的类。
当 server 引用 Web 服务器对象时,执行 WebServer 的 start();当它引用数据库服务器对象时,执行 DatabaseServer 的 start()。
这就是多态在这个例子中的表现:调用方式相同,具体行为可以不同。
再想想,如果以后需要增加文件服务器,我们只需要定义 FileServer,让它继承 Server,再提供自己的 start() 方法,就可以把新对象放进 servers 列表中。
原来的 for 循环完全不需要修改。
这也是多态的实际价值:调用方不必为每一种服务器编写一套独立的处理逻辑,程序更容易扩展。
继承、方法重写和多态有什么区别
这三个概念容易混淆,我们用服务器案例对照理解。
| 概念 | 主要解决的问题 | 服务器案例 |
|---|---|---|
| 继承 | 如何复用已有代码 | 不同服务器共享 Server 的初始化方法 |
| 方法重写 | 子类如何定制已有方法 | 不同服务器分别定义自己的 start() |
| 多态 | 如何用统一的方式处理不同对象 | 循环中统一调用 server.start() |
可以把它们的关系理解为:
flowchart TD
A["定义 Server 父类"] --> B["子类继承共同属性和方法"]
B --> C["子类根据需求重写 start()"]
C --> D["不同类型的对象放入同一个列表"]
D --> E["统一调用 server.start()"]
E --> F["不同对象执行各自的行为"]继承为代码复用提供了基础,方法重写让子类能够定制行为,多态则让程序可以使用统一的调用方式处理不同对象。
不过,需要注意:继承并不意味着子类必须重写父类的每个方法;而多态也不只存在于继承关系中,只要不同对象提供了调用方需要的方法,就可以采用统一的调用方式处理它们。后面学习更多 Python 知识时,我们还会逐步接触这种用法。
动手实践:完成一个服务器分类与启动信息管理程序
概念理解之后,最好自己动手写一遍。下面我们从空文件开始,完成一个小项目。
任务目标:
编写一个服务器管理程序,要求能够:
- 定义公共的
Server父类,保存服务器名称和 IP 地址。 - 定义
WebServer、DatabaseServer和FileServer三种子类。 - 让三种子类继承父类的初始化方法。
- 为每种子类编写自己的
start()方法。 - 创建三种服务器对象,保存到同一个列表中。
- 使用一个
for循环统一输出每台服务器的启动信息。 - 使用
isinstance()和issubclass()验证对象类型与继承关系。
创建 Python 文件
在你平时保存 Python 练习代码的目录下,新建一个文件:
1 | server_manager.py |
这里的文件名可以自行选择,但后续运行命令时要与实际文件名保持一致。
编写完整程序
将下面的代码完整写入 server_manager.py:
1 | class Server: |
代码中使用了三引号编写的说明文字,例如:
1 | """Web 服务器。""" |
这类文字叫作文档字符串,通常用于说明类或函数的用途。它不是实现服务器启动功能的代码,不影响我们对继承和多态的理解。
运行程序
打开终端或命令提示符,进入 server_manager.py 所在的目录,然后执行:
1 | python server_manager.py |
如果你的环境需要使用 python3 命令,则执行:
1 | python3 server_manager.py |
预期输出如下:
1 | === 服务器启动信息 === |
对照输出检查是否理解
先观察三行服务器启动信息。虽然三个对象的类型不同,但程序使用同一个 for 循环和同一种方法调用,就完成了全部输出。这体现了多态。
再观察最后四行。web01 是 WebServer 对象,而 WebServer 又继承了 Server,所以前两个对象类型判断都为 True。
issubclass(WebServer, Server) 的结果是 True,因为子类确实继承了父类;反过来,issubclass(Server, WebServer) 的结果是 False,因为父类并不是子类的子类。
完成扩展任务
程序运行成功后,再尝试进行以下修改。
扩展任务:增加缓存服务器
新增一个 CacheServer 类,要求:
- 继承
Server。 - 重写
start()方法,输出类似缓存服务器 cache01 已启动,IP:192.168.1.30的信息。 - 创建
CacheServer对象,并将它加入servers列表。 - 重新运行程序,确认缓存服务器的信息也能被输出。
扩展任务:验证空类继承
暂时删除 FileServer 中的 start() 方法,让它只保留类定义和文档字符串,或者使用 pass 作为类体。
再次运行程序,观察文件服务器的启动信息会发生什么变化。
这是因为当子类没有重写 start() 时,它会使用父类 Server 中的实现。因此,输出将变成通用的服务器启动信息,而不是文件服务器专属的信息。
通过这个实验,你可以亲自验证:继承不仅能复用初始化方法,也能复用其他普通方法;只有子类重新定义方法时,才会使用子类自己的实现。
写在最后
今天,我们从 Linux 服务器管理这个实际问题出发,学习了 Python 面向对象编程中的继承与多态。
继承让子类复用父类的属性和方法;空类继承可以先明确对象的类别,为分类处理和后续扩展打下基础;方法重写让子类能够定制自己的行为;多态则让不同类型的对象能够通过统一的调用方式执行各自的操作。
我们还通过 isinstance() 和 issubclass() 验证了对象类型与类之间的继承关系,并完成了一个可以运行的服务器管理小项目。
下一篇,我们将继续学习 Python 内置函数 type()、isinstance() 和 issubclass(),进一步理解如何获取对象的类型,以及如何判断类与类之间的关系。
