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 到底是什么?一文搞懂常见异常
管理服务器时,我们经常需要记录服务器的 IP 地址、用途、运行状态等信息。如果有几十台甚至几百台服务器,怎样才能让这些数据更容易管理?
前面我们已经学习了 Python 的类和对象。本篇继续深入,先把类与对象的关系彻底弄清楚,再认识 Python 的特殊方法,重点理解
__init__()和__str__()。学完之后,你将能够编写一个简单的服务器类,创建多个服务器对象,并让
print()直接输出清晰的服务器信息。
从实际运维问题开始
假设你负责维护 3 台 Linux 服务器:
| 服务器 | IP 地址 | 用途 | 状态 |
|---|---|---|---|
| web01 | 192.168.1.10 | Web 服务器 | running |
| db01 | 192.168.1.20 | 数据库服务器 | running |
| cache01 | 192.168.1.30 | 缓存服务器 | stopped |
如果只有几台服务器,直接把信息写在表格里,或者保存在几个变量中,都没有太大问题。
但如果服务器数量增加到 200 台呢?
我们可能需要编写 Python 程序,定期检查服务器状态、整理资产信息,甚至生成巡检报告。
这时,就需要思考一个问题:怎样把一台服务器的相关数据组织在一起,让程序更方便地使用这些数据?
我们先用最简单的方法试试。
1 | server_name = "web01" |
运行结果:
1 | web01 |
这段代码没有问题。我们用 4 个变量保存了一台服务器的不同信息。
不过,变量之间并没有直接的组织关系。如果继续记录其他服务器,就需要不断增加变量,代码也会越来越难管理。
当然,我们可以使用字典、列表等数据结构组织这些信息。上一篇已经介绍过类和对象,本篇就沿着这个方向继续学习:如何使用类定义服务器应该具备的数据和行为,再通过类创建一个个具体的服务器对象。
理解类与对象的关系
先把这两个概念弄清楚,后面的代码才容易理解。
什么是类?
我们可以把类理解为一份描述:某种事物应该具备哪些数据,以及能够执行哪些操作。
例如,设计一个服务器类时,我们可以规定每台服务器都可以记录:
- 服务器名称
- IP 地址
- 服务器用途
- 当前运行状态
我们还可以为服务器定义一些操作,例如显示服务器信息、检查状态等。
在 Python 中,可以使用 class 关键字定义类。
1 | class Server: |
这里定义了一个名为 Server 的类。
pass 是 Python 的一个占位语句,表示这里暂时不执行任何操作。我们先用它定义一个最简单的类,后面再逐步添加内容。
什么是对象?
有了 Server 类之后,我们就可以根据它创建具体的服务器对象。
例如:
1 | class Server: |
这段代码创建了两个 Server 对象。
其中:
web01引用第一个对象。db01引用第二个对象。
这里的“引用”可以先简单理解为:变量保存了一个对象的引用,我们可以通过这个变量访问对应的对象。
虽然两个对象都来自同一个 Server 类,但它们是分别创建出来的两个对象。
可以用下面的图来理解:
flowchart TD
A["Server 类"] --> B["创建 web01 对象"]
A --> C["创建 db01 对象"]
B --> D["第一台服务器的具体数据"]
C --> E["第二台服务器的具体数据"]类和对象的关系,可以类比为服务器资产管理规范与具体的服务器资产:
- 类:规定服务器对象可以具备什么数据和行为。
- 对象:根据类创建出来的具体实例,可以保存自己的数据。
一个类可以创建多个对象。每个对象可以有相同类型的数据,但保存的具体值可以不同。
需要注意,类不只是数据模板,它还可以定义操作这些数据的方法。后面我们会逐步看到这一点。
为服务器对象添加数据
现在,我们希望每个 Server 对象都能保存服务器名称、IP 地址和运行状态。
在上一篇中,我们已经学习过 __init__()。这里先结合运维场景复习它的作用。
修改代码:
1 | class Server: |
这段代码看起来比前面的例子复杂了一些,我们分开理解。
定义 __init__() 方法
1 | def __init__(self, name, ip, status): |
这里定义了一个名为 __init__() 的特殊方法。
先认识方法中的几个参数:
self:表示当前正在初始化的对象。name:接收服务器名称。ip:接收服务器 IP 地址。status:接收服务器状态。
接下来,看这行代码:
1 | self.name = name |
等号左边的 self.name 表示当前对象的 name 属性,等号右边的 name 表示传入的参数。
假设传入的服务器名称是 "web01",那么执行这行代码后,当前对象的 name 属性就保存了 "web01"。
另外两行也是同样的道理:
1 | self.ip = ip |
它们分别把 IP 地址和运行状态保存到当前对象的属性中。
纠正一个容易产生的误解:__init__() 并不负责创建对象
当我们看到下面这行代码时:
1 | web01 = Server("web01", "192.168.1.10", "running") |
很容易认为,__init__() 就是创建对象的方法。
严格来说,这种理解并不准确。
Python 创建对象和初始化对象,是两个不同的步骤。
当我们调用 Server(...) 时,Python 会先创建一个新的对象,再自动调用这个对象的 __init__() 方法,使用传入的参数初始化对象的数据。
从概念上看,过程如下:
flowchart TD
A["调用 Server(...)"] --> B["Python 创建新对象"]
B --> C["自动调用 __init__()"]
C --> D["参数接收服务器信息"]
D --> E["将数据保存到对象属性"]
E --> F["初始化完成"]在 Python 的底层机制中,__new__() 参与对象的创建,而 __init__() 负责初始化已经创建的对象。
初学阶段暂时不需要自己编写 __new__()。只需要记住这个区别:
创建对象和初始化对象不是一回事。__init__() 负责初始化,而不是负责创建对象本身。
看看两个对象分别保存了什么
前面的代码创建了两个对象:
1 | web01 = Server("web01", "192.168.1.10", "running") |
第一个对象保存的信息是:
- 名称:
web01 - IP 地址:
192.168.1.10 - 状态:
running
第二个对象保存的信息是:
- 名称:
db01 - IP 地址:
192.168.1.20 - 状态:
running
它们来自同一个 Server 类,但拥有各自的实例数据。
我们可以通过对象的属性查看这些数据:
1 | print(web01.name) |
运行结果:
1 | web01 |
这样,我们就完成了服务器数据的组织。
但现在还有一个小问题:如果我们直接打印整个对象,会发生什么?
为什么 print() 打印对象时不够直观?
在上面的代码后面添加两行:
1 | print(web01) |
运行程序后,可能会看到类似下面的结果:
1 | <__main__.Server object at 0x000001ABCDEF1234> |
具体输出会因运行环境和对象不同而变化。
这里的内容不是程序出错,而是因为我们还没有定义服务器对象应该怎样显示成一段可读的文本。
Python 知道 web01 是一个 Server 对象,但它并不知道我们希望输出的是服务器名称、IP 地址和运行状态。
我们当然可以每次都这样打印:
1 | print(web01.name, web01.ip, web01.status) |
这完全可行。
但如果每次都要手动列出多个属性,不仅代码重复,而且每次想调整输出格式,都需要修改相应的代码。
有没有办法让 Python 知道:当我们打印一个服务器对象时,就自动按照指定格式显示它的信息?
这就需要用到 __str__()。
使用 __str__() 定义对象的显示格式
__str__() 是 Python 的一个特殊方法。
它的主要作用是:定义对象转换成字符串时,应该返回什么内容。
我们在 Server 类中添加这个方法:
1 | class Server: |
运行结果:
1 | 服务器名称:web01,IP:192.168.1.10,状态:running |
现在,输出结果清晰多了。
我们逐步看看新增加的代码。
理解 __str__() 的定义
1 | def __str__(self): |
这里的 self 仍然表示当前对象。
当我们打印 web01 时,self.name 就会取得 web01 对象的名称,self.ip 取得它的 IP 地址,self.status 取得它的运行状态。
当我们打印 db01 时,这些属性就会来自 db01 对象。
代码中的 f"..." 是 f-string,可以把大括号中的表达式计算结果放进字符串。
例如:
1 | name = "web01" |
输出结果:
1 | 服务器名称:web01,IP:192.168.1.10 |
这里的 __str__() 也是利用了同样的机制,只不过插入字符串的数据来自对象的属性。
为什么 print(web01) 会自动使用 __str__()?
当我们执行:
1 | print(web01) |
Python 需要把对象转换成字符串,才能将内容显示出来。在这个过程中,Python 会使用该对象的 __str__() 方法获取字符串表示。
整个过程可以理解为:
flowchart TD
A["执行 print(web01)"] --> B["Python 需要显示对象"]
B --> C["使用对象的 __str__()"]
C --> D["读取对象的名称、IP 和状态"]
D --> E["返回格式化后的字符串"]
E --> F["print() 显示服务器信息"]我们不需要手动调用 __str__()。只要定义好这个方法,在使用 print() 打印对象时,Python 就会自动使用它。
另外,str(web01) 也可以触发这个字符串转换过程。
例如:
1 | result = str(web01) |
输出结果:
1 | 服务器名称:web01,IP:192.168.1.10,状态:running |
type(result) 用于查看数据类型。输出 <class 'str'>,说明 result 保存的是字符串。
因此,__str__() 应当返回字符串。如果返回的是整数、列表等其他类型,Python 在需要使用该方法时会抛出 TypeError。
把特殊方法用在实际运维场景中
现在,我们已经知道如何定义服务器类、创建服务器对象,以及自定义对象的显示格式。
把这些知识放在一起,就能编写一个简单的服务器资产清单程序。
下面增加一台已经停止运行的缓存服务器,看看多个对象如何配合使用。
1 | class Server: |
运行结果:
1 | 名称:web01 | IP:192.168.1.10 | 用途:Web 服务器 | 状态:running |
这段代码包含了几个我们已经学过的知识点。
使用类定义服务器信息
Server 类定义了每台服务器应该保存的四项数据:
name:服务器名称。ip:服务器 IP 地址。role:服务器用途。status:服务器运行状态。
通过 __init__(),每个服务器对象都可以保存自己的数据。
使用列表保存多个对象
1 | servers = [web01, db01, cache01] |
这里创建了一个列表,将三个服务器对象放在一起。
列表中的每一项都是一个 Server 对象,而不是服务器名称对应的字符串。
使用循环逐个显示服务器信息
1 | for server in servers: |
for 循环会依次取出列表中的对象,并把当前对象赋给变量 server。
每次执行 print(server) 时,Python 都会使用当前对象的 __str__() 方法生成对应的字符串,再将其显示出来。
这意味着,无论列表中有 3 台服务器还是 300 台服务器,只要这些对象已经保存在列表中,就可以使用同样的循环逐个输出它们的信息。
当然,这只是一个用于演示类和对象的简单资产清单。实际生产环境中,服务器信息通常还需要从配置文件、资产管理系统、云平台 API 或监控系统中获取,而不能仅依赖手动写在代码中的数据。
特殊方法还有哪些?
我们目前重点学习了 __init__() 和 __str__()。
它们都属于 Python 的特殊方法,也常被称为“双下方法”,因为方法名通常以两个下划线开头、两个下划线结尾。
之所以特殊,是因为 Python 会在相应的操作发生时,按照规定自动使用这些方法。
例如:
| 特殊方法 | 主要作用 | 常见触发场景 |
|---|---|---|
__init__() | 初始化对象 | 调用类创建对象时 |
__str__() | 定义对象的字符串表示 | 使用 print() 或 str() 转换对象时 |
__len__() | 定义对象在求长度时的行为 | 调用 len() 时 |
__add__() | 定义对象参与加法运算时的行为 | 使用 + 运算符时 |
这里不需要急着掌握所有特殊方法。
对于刚接触类和对象的同学来说,先理解两个核心问题就够了:
- 创建对象后,怎样初始化它的数据?
- 当程序需要显示一个对象时,怎样让输出内容更容易理解?
__init__() 和 __str__() 分别帮助我们解决了这两个问题。
动手练习
现在,请你自己完成一个小练习。
假设我们要管理 Linux 服务器的资产信息,请定义一个名为 Server 的类。
要求如下:
- 使用
__init__()初始化服务器名称、IP 地址和运行状态。 - 使用
__str__()返回清晰的服务器信息。 - 创建两台服务器对象,分别表示一台 Web 服务器和一台数据库服务器。
- 将两个对象放进列表,通过
for循环打印所有服务器的信息。
可以先尝试独立编写,再参考下面的答案。
参考答案
1 | class Server: |
运行结果:
1 | web01 | 192.168.1.10 | running |
如果你能够独立完成这个练习,就已经把类、对象、实例数据、__init__() 和 __str__() 连接起来了。
写在最后
本篇从 Linux 服务器资产管理的场景出发,进一步理解了类与对象的关系,并学习了两个重要的特殊方法。
需要记住的内容有:
- 类用于定义一类对象的数据和行为。
- 对象是根据类创建出来的具体实例,同一个类可以创建多个对象。
__init__()用于初始化对象的数据。Python 会先创建对象,再调用它完成初始化。__str__()用于定义对象转换成字符串时的表示形式,让print()输出更清晰的信息。- 特殊方法遵循 Python 规定的命名和调用机制,可以让对象更自然地参与 Python 的各种操作。
下一篇,我们将学习类变量,看看为什么同一个类创建的多个对象,除了保存各自的数据之外,还可以共享某些数据,以及类变量和实例变量之间有什么区别。
