1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
  23. Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
  24. Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
  25. Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
  26. Python 系列(二十六) Python 模块入门,从 import 到模块复用
  27. Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
  28. Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
  29. Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
  30. Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
  31. Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
  32. Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
  33. Python 系列(三十三) 什么是类和对象?零基础理解 Python class
  34. Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
  35. Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
  36. Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
  37. Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
  38. Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
  39. Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
  40. Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
  41. Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
  42. Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常

前面我们学习了 Python 的类和对象,知道可以通过类描述一类事物的共同特征,再创建多个对象来表示不同的实际事物。

假设现在需要管理 Linux 服务器,既有 Web 服务器,也有数据库服务器,还有缓存服务器。它们都有服务器名称、IP 地址等共同信息,但提供的服务又各不相同。

如果每种服务器都从头编写一遍代码,不仅重复工作多,后期维护也很麻烦。

有没有办法让不同类型的服务器复用共同的代码,同时保留各自的特点?

这就是今天要学习的两个概念:继承与多态。

从重复代码说起

先来看一个实际问题。

我们需要管理一台 Web 服务器和一台数据库服务器。两种服务器都有名称和 IP 地址,因此可以分别定义两个类。

创建一个名为 server_demo.py 的文件,写入下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class WebServer:
def __init__(self, name, ip):
self.name = name
self.ip = ip

def start(self):
return f"Web 服务器 {self.name} 已启动,IP:{self.ip}"


class DatabaseServer:
def __init__(self, name, ip):
self.name = name
self.ip = ip

def start(self):
return f"数据库服务器 {self.name} 已启动,IP:{self.ip}"


web01 = WebServer("web01", "192.168.1.10")
db01 = DatabaseServer("db01", "192.168.1.20")

print(web01.start())
print(db01.start())

这里先不急着记忆每一行代码,我们先关注两个类的共同点。

它们都使用 __init__() 保存服务器名称和 IP 地址,也都提供了 start() 方法。区别只是启动时输出的文字不同。

运行程序:

1
python server_demo.py

输出结果:

1
2
Web 服务器 web01 已启动,IP:192.168.1.10
数据库服务器 db01 已启动,IP:192.168.1.20

程序虽然可以正常运行,但两个类中存在重复代码。

如果以后还要增加缓存服务器、文件服务器、消息队列服务器,我们可能需要反复编写保存名称和 IP 地址的代码。

能不能把这些共同的内容集中起来,让其他类直接使用?

当然可以。这就需要学习继承。

理解继承:让子类复用父类的代码

在现实生活中,Web 服务器和数据库服务器虽然用途不同,但它们都属于服务器。

我们可以把这种关系理解为:

  • 父类:描述多个子类共同拥有的属性和方法。
  • 子类:在父类的基础上,继承已有的属性和方法,还可以增加自己的功能或重新定义某些行为。

Python 中的继承写法很简单:定义子类时,在类名后面的括号中写上父类的名称。

先把 server_demo.py 修改为下面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Server:
def __init__(self, name, ip):
self.name = name
self.ip = ip


class WebServer(Server):
pass


web01 = WebServer("web01", "192.168.1.10")

print(web01.name)
print(web01.ip)

运行程序:

1
python server_demo.py

输出结果:

1
2
web01
192.168.1.10

第一次看到这里,可能会有一个疑问:WebServer 类明明没有定义 __init__(),为什么创建对象时还可以传入名称和 IP 地址?

关键就在这里:

1
2
class WebServer(Server):
pass

WebServer(Server) 表示 WebServer 继承了 Server。

因此,WebServer 可以使用父类 Server 中定义的 __init__() 方法。创建 WebServer 对象时,Python 会调用这个继承而来的方法,将名称和 IP 地址保存到对象中。

这里的 pass 是 Python 的占位语句,表示暂时不执行任何操作。它让我们可以先定义一个空的类,而不必马上编写方法或属性。

这段代码说明:子类即使没有重新定义初始化方法,也可以使用从父类继承来的方法。

接下来,我们不只是观察程序能否运行,还要验证继承关系是否真的存在。

动手验证:子类真的继承了父类吗

程序输出了名称和 IP 地址,只能说明对象可以正常使用这些属性。我们还可以直接检查类之间的关系。

在 server_demo.py 中保留刚才的 Server 和 WebServer 定义,并把最后几行改成:

1
2
3
4
5
6
web01 = WebServer("web01", "192.168.1.10")

print(isinstance(web01, WebServer))
print(isinstance(web01, Server))
print(issubclass(WebServer, Server))
print(issubclass(Server, WebServer))

运行程序:

1
python server_demo.py

输出结果:

1
2
3
4
True
True
True
False

这四个结果分别是什么意思?

先看第一行:

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)对象是否属于 WebServerTrue
isinstance(web01, Server)对象是否属于 Server 或其子类True
issubclass(WebServer, Server)WebServer 是否是 Server 的子类True
issubclass(Server, WebServer)Server 是否是 WebServer 的子类False

这里先记住一个实用区别:isinstance() 检查对象,issubclass() 检查类。

下一篇会专门学习这些内置函数,包括如何判断对象的类型,以及如何判断两个类之间的继承关系。

空类继承有什么实际意义

现在我们已经看到,下面的代码即使没有定义任何方法,也能正常工作:

1
2
class WebServer(Server):
pass

那么问题来了:既然 WebServer 什么都没写,为什么还要定义它?直接使用 Server 不就行了吗?

这个问题非常重要。

空类继承并不是为了少写一行代码,而是为了先明确对象的类别,同时复用父类的功能,为以后扩展留下空间。

我们用一个更贴近运维工作的例子来理解。

假设公司有多台服务器,其中既有普通服务器,也有 Web 服务器。现在暂时不需要给 Web 服务器增加专属方法,但我们希望程序能够区分它们。

可以这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Server:
def __init__(self, name, ip):
self.name = name
self.ip = ip


class WebServer(Server):
pass


server01 = Server("server01", "192.168.1.5")
web01 = WebServer("web01", "192.168.1.10")

servers = [server01, web01]

for server in servers:
print(server.name, isinstance(server, WebServer))

运行结果:

1
2
server01 False
web01 True

注意,这里没有给 WebServer 增加任何新方法,但程序已经能够区分普通服务器和 Web 服务器。

这是因为两者虽然都具备父类提供的名称和 IP 地址,但它们属于不同的类。

在实际项目中,这种区别可以用于分类统计、选择不同的处理流程,或者为后续扩展提供基础。例如,以后可以只对 Web 服务器执行网站服务检查,而对数据库服务器执行数据库服务检查。

当然,如果一个类既没有独立的业务含义,也不需要特殊处理或未来扩展,就没有必要为了使用继承而刻意创建空子类。 是否创建子类,应该由实际需求决定。

让子类拥有自己的功能

前面我们学习了如何通过继承复用父类的初始化方法。

但是,不同类型的服务器不仅名称和 IP 地址不同,执行的操作也可能不同。

例如,Web 服务器提供网站服务,数据库服务器负责数据库服务,缓存服务器负责缓存服务。

我们可以先在父类中定义一个通用的 start() 方法,再让不同的子类根据自己的需要重新定义它。

修改 server_demo.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Server:
def __init__(self, name, ip):
self.name = name
self.ip = ip

def start(self):
return f"服务器 {self.name} 已启动,IP:{self.ip}"


class WebServer(Server):
def start(self):
return f"Web 服务器 {self.name} 已启动,IP:{self.ip}"


class DatabaseServer(Server):
def start(self):
return f"数据库服务器 {self.name} 已启动,IP:{self.ip}"


web01 = WebServer("web01", "192.168.1.10")
db01 = DatabaseServer("db01", "192.168.1.20")

print(web01.start())
print(db01.start())

运行结果:

1
2
Web 服务器 web01 已启动,IP:192.168.1.10
数据库服务器 db01 已启动,IP:192.168.1.20

这里有一个新概念:方法重写。

方法就是定义在类中的函数。例如,前面定义的 start() 就是一个方法。

当子类重新定义了一个与父类同名的方法时,就可以改变该子类调用这个方法时的行为。这种做法叫作方法重写。

在上面的例子中:

  • Server 定义了通用的 start() 方法。
  • WebServer 重写了 start(),输出 Web 服务器的信息。
  • DatabaseServer 重写了 start(),输出数据库服务器的信息。

如果子类没有重写某个方法,就可以直接使用父类继承而来的实现。

需要注意的是,这里展示的是程序输出信息,并没有真正启动 Linux 服务。真实的服务管理还需要调用操作系统命令或相应的 API。

理解多态:同一个方法,不同的表现

现在,我们已经知道不同的子类可以拥有自己的 start() 方法。

那么,能不能把不同类型的服务器放到同一个列表中,然后用同一段代码依次调用它们的启动方法?

可以。这就是多态最直观的应用。

先看完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Server:
def __init__(self, name, ip):
self.name = name
self.ip = ip

def start(self):
return f"服务器 {self.name} 已启动,IP:{self.ip}"


class WebServer(Server):
def start(self):
return f"Web 服务器 {self.name} 已启动,IP:{self.ip}"


class DatabaseServer(Server):
def start(self):
return f"数据库服务器 {self.name} 已启动,IP:{self.ip}"


class CacheServer(Server):
def start(self):
return f"缓存服务器 {self.name} 已启动,IP:{self.ip}"


servers = [
WebServer("web01", "192.168.1.10"),
DatabaseServer("db01", "192.168.1.20"),
CacheServer("cache01", "192.168.1.30"),
]

for server in servers:
print(server.start())

运行结果:

1
2
3
Web 服务器 web01 已启动,IP:192.168.1.10
数据库服务器 db01 已启动,IP:192.168.1.20
缓存服务器 cache01 已启动,IP:192.168.1.30

我们一步一步分析这段代码。

首先,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Server:
"""所有服务器的公共父类。"""

def __init__(self, name, ip):
self.name = name
self.ip = ip

def start(self):
return f"服务器 {self.name} 已启动,IP:{self.ip}"


class WebServer(Server):
"""Web 服务器。"""

def start(self):
return f"Web 服务器 {self.name} 已启动,IP:{self.ip}"


class DatabaseServer(Server):
"""数据库服务器。"""

def start(self):
return f"数据库服务器 {self.name} 已启动,IP:{self.ip}"


class FileServer(Server):
"""文件服务器。"""

def start(self):
return f"文件服务器 {self.name} 已启动,IP:{self.ip}"


# 创建不同类型的服务器对象
servers = [
WebServer("web01", "192.168.1.10"),
DatabaseServer("db01", "192.168.1.20"),
FileServer("file01", "192.168.1.40"),
]

# 使用统一的方式输出启动信息
print("=== 服务器启动信息 ===")

for server in servers:
print(server.start())

# 验证对象与类之间的关系
print("\n=== 类型与继承关系验证 ===")

web01 = servers[0]

print("web01 是 WebServer 对象:", isinstance(web01, WebServer))
print("web01 是 Server 对象:", isinstance(web01, Server))
print("WebServer 继承 Server:", issubclass(WebServer, Server))
print("Server 继承 WebServer:", issubclass(Server, WebServer))

代码中使用了三引号编写的说明文字,例如:

1
"""Web 服务器。"""

这类文字叫作文档字符串,通常用于说明类或函数的用途。它不是实现服务器启动功能的代码,不影响我们对继承和多态的理解。

运行程序

打开终端或命令提示符,进入 server_manager.py 所在的目录,然后执行:

1
python server_manager.py

如果你的环境需要使用 python3 命令,则执行:

1
python3 server_manager.py

预期输出如下:

1
2
3
4
5
6
7
8
9
10
=== 服务器启动信息 ===
Web 服务器 web01 已启动,IP:192.168.1.10
数据库服务器 db01 已启动,IP:192.168.1.20
文件服务器 file01 已启动,IP:192.168.1.40

=== 类型与继承关系验证 ===
web01 是 WebServer 对象: True
web01 是 Server 对象: True
WebServer 继承 Server: True
Server 继承 WebServer: False

对照输出检查是否理解

先观察三行服务器启动信息。虽然三个对象的类型不同,但程序使用同一个 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(),进一步理解如何获取对象的类型,以及如何判断类与类之间的关系。