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 到底是什么?一文搞懂常见异常
在 Linux 运维工作中,我们可能需要使用 Python 管理几十台甚至几百台服务器。
如果每创建一个服务器对象,就想记录累计创建了多少个对象,应该怎么做?
是给每台服务器单独保存一个计数值,还是让所有服务器共同使用一个计数值?
这正是 Python 类变量要解决的问题。本篇从服务器资产管理的实际场景出发,带你理解实例变量与类变量的区别,掌握类变量的定义、访问和修改方式。
从实际运维问题开始
假设我们正在编写一个简单的服务器资产管理程序,需要记录以下服务器信息:
| 服务器名称 | IP 地址 | 用途 | 状态 |
|---|---|---|---|
| web01 | 192.168.1.10 | Web 服务器 | running |
| web02 | 192.168.1.11 | Web 服务器 | running |
| db01 | 192.168.1.20 | 数据库服务器 | stopped |
前面学习类和对象时,我们已经知道,可以定义一个 Server 类,再根据这个类创建多个服务器对象。
1 | class Server: |
运行结果:
1 | web01 |
这段代码中,我们创建了三个 Server 对象,每个对象都保存了自己的服务器名称和 IP 地址。
例如,web01.name 保存的是 web01,而 db01.name 保存的是 db01。
这些数据属于各自的对象,因此称为实例变量。
不过,现在出现了一个新需求:我们希望程序能够记录累计创建了多少个服务器对象。
创建第一个对象,计数变成 1;创建第二个对象,计数变成 2;创建第三个对象,计数变成 3。
这个计数值与某一台具体的服务器没有直接关系,而是与整个 Server 类的对象初始化过程有关。
能不能让多个服务器对象共同使用同一个计数值?
要解决这个问题,就需要认识类变量。
为什么实例变量不适合直接完成共享计数
先看一种可能的写法:
1 | class Server: |
这里的 self.count = 0 是什么意思?
还记得 self 表示什么吗?它表示当前正在初始化的对象。
因此,创建 web01 时,Python 会在 web01 对象上设置一个 count 属性;创建 web02 时,又会在 web02 对象上设置一个独立的 count 属性。
每个对象的 count 都从 0 开始,互不影响。
这正是实例变量的特点:每个对象都可以保存自己的数据。
但我们想要的是一个统一的计数值,而不是每台服务器各自保存一个互不相关的计数器。
接下来,我们把计数值放到类中。
在类中定义变量
Python 不仅允许我们通过 self.属性名 为对象保存数据,也允许直接在类的代码块中定义变量。
例如:
1 | class Server: |
注意这行代码:
1 | count = 0 |
它直接写在 class Server: 的代码块中,没有写在 __init__() 方法内部,也没有使用 self.count。
这种定义在类命名空间中的变量,称为类变量。
可以先这样理解:
- 实例变量通常保存某个对象自己的数据。
- 类变量保存与类相关的数据,可以通过类或实例访问。
例如,服务器名称和 IP 地址属于具体的服务器,适合保存在实例变量中。
而一个简单程序中累计执行了多少次服务器对象初始化,可以使用类变量来记录。
flowchart TD
A["Server 类"] --> B["类变量:count"]
A --> C["web01 对象"]
A --> D["web02 对象"]
A --> E["db01 对象"]
C --> F["实例变量:name、ip"]
D --> G["实例变量:name、ip"]
E --> H["实例变量:name、ip"]
B -. "可以访问" .-> C
B -. "可以访问" .-> D
B -. "可以访问" .-> E同一个类可以创建多个对象。每个对象可以保存自己的实例变量,同时也可以访问所属类中的类变量。
使用类变量累计对象初始化次数
现在,我们把 count 用起来。
1 | class Server: |
运行结果:
1 | 3 |
为什么结果是 3?
关键在于这行代码:
1 | Server.count += 1 |
这里的 Server.count 表示访问 Server 类中的 count 变量。
+= 1 表示在原来的数值上加 1,再将结果赋回去。它相当于:
1 | Server.count = Server.count + 1 |
我们把执行过程拆开看:
- 定义
Server类时,count的初始值是0。 - 创建
web01时,初始化方法中的计数语句执行,count变成1。 - 创建
web02时,计数语句再次执行,count变成2。 - 创建
db01时,计数语句再次执行,count变成3。
整个过程如下:
flowchart TD
A["定义 Server 类"] --> B["count 初始值为 0"]
B --> C["创建 web01"]
C --> D["count 变为 1"]
D --> E["创建 web02"]
E --> F["count 变为 2"]
F --> G["创建 db01"]
G --> H["count 变为 3"]
H --> I["读取 Server.count"]这里有一个需要说准确的地方。
Server.count 记录的是计数语句累计执行的次数,不一定等于当前仍然存在的服务器对象数量。
在这个例子中,每次创建对象都会让 __init__() 执行计数语句,所以最终得到 3。
但如果后续删除某个对象,计数值不会自动减一;如果初始化过程中在计数语句之前发生异常,计数值也不会增加。因此,这个变量更准确地说是一个简单的累计计数器,而不是实时统计当前存活对象数量的机制。
在入门示例中,它可以帮助我们理解类变量如何被多个对象共同使用,但不能直接当作生产环境中的实时资产统计方案。
理解属性查找:为什么多个对象可以读取同一个类变量
前面我们使用:
1 | print(Server.count) |
读取类变量,结果是 3。
那下面两行会得到什么结果?
1 | print(web01.count) |
它们也会输出 3。
为什么?
要理解这个现象,需要先了解 Python 访问属性时的基本查找过程。
读取属性时,Python 会先查找对象自身
当我们执行:
1 | web01.count |
Python 会先检查 web01 对象自身有没有 count 属性。
如果有,就使用对象自身的属性值。
如果没有,Python 会继续查找它所属的类 Server 中是否定义了 count。
如果类中也没有,普通的属性查找还可能沿着继承关系继续查找父类。
因此,在我们当前的例子中:
web01自身没有count属性。Server类中定义了count,值为3。- 所以
web01.count读取到的是Server.count的值。
web02.count 也是同样的道理。
可以将这个过程简化为:
flowchart TD
A["读取 web01.count"] --> B{"web01 自身有 count 吗?"}
B -->|有| C["返回对象自身的 count"]
B -->|没有| D{"Server 类中有 count 吗?"}
D -->|有| E["返回类中的 count"]
D -->|没有| F["继续查找继承链或触发属性错误"]这里说的是普通实例属性的基本查找过程。Python 还存在描述符等更高级的属性查找机制,后续遇到时再深入学习即可。
现在只需要记住:通过对象读取属性时,如果对象自身没有该属性,Python 通常会继续从所属类中查找。
这就是多个对象能够访问同一个类变量的原因。
理解赋值:为什么修改对象属性不会自动修改类变量
知道属性查找过程后,我们再看一个非常容易混淆的问题。
假设我们执行:
1 | web01.count = 100 |
你可能会认为,这行代码会把 Server.count 从 3 修改为 100。
但实际上并不是这样。
对于当前这个没有自定义属性赋值行为的普通对象,执行 web01.count = 100,通常会在 web01 对象自身设置一个名为 count 的实例属性。
它不会因为这个名字与类变量相同,就自动修改 Server.count。
为了看清楚这个区别,我们直接在前面已经创建好的对象上进行实验:
1 | print(Server.count) |
运行结果:
1 | 3 |
我们逐步分析。
赋值之前
Server.count 的值为 3。
web01 和 web02 自身都没有 count 属性,所以读取它们的 count 时,Python 会继续查找 Server 类。
因此,三个表达式都得到 3。
执行赋值之后
1 | web01.count = 100 |
这行代码为 web01 设置了自己的 count 属性,值为 100。
此时:
Server.count仍然是3。web01.count优先读取对象自身的属性,得到100。web02自身仍然没有count属性,因此读取到类变量Server.count,得到3。
也就是说,web01 的同名实例属性遮蔽了通过这个对象读取类变量的结果,但并没有删除或修改类变量。
整个关系可以理解为:
flowchart TD
A["Server 类"] --> B["类变量 count = 3"]
C["web01 对象"] --> D["实例变量 count = 100"]
E["web02 对象"] -. "自身没有 count" .-> B
C -. "读取 count 时优先使用自身属性" .-> D
E -. "读取 count 时继续查找所属类" .-> B如果确实要修改类变量,应该明确使用类名:
1 | Server.count = 100 |
这样修改的才是 Server 类中的 count 变量。
要注意,如果对象已经有同名的实例变量,例如刚才的 web01.count,修改 Server.count 不会自动删除 web01 自己的 count 属性。此时,读取 web01.count 仍会优先得到对象自身保存的值。
因此,在实际代码中,修改类变量时推荐使用 类名.变量名,不要把 对象.变量名 = 新值 当成修改类变量的方式。
在 Linux 运维案例中使用类变量
现在,我们把这些知识放到一个完整的小程序中。
假设我们要编写一个简单的服务器资产清单,记录服务器名称、IP 地址和运行状态,同时累计统计初始化了多少个服务器对象。
1 | class Server: |
运行结果:
1 | web01 | 192.168.1.10 | running |
这里整合了前面学习过的知识:
Server类定义服务器对象的数据和行为。__init__()初始化每台服务器自己的名称、IP 地址和状态。Server.count累计记录计数语句执行的次数。__str__()定义服务器对象转换成字符串时的显示格式。- 列表保存多个服务器对象,
for循环逐个输出服务器信息。
在这个例子中,名称、IP 地址和状态都属于具体服务器,因此保存在实例变量中;累计计数值则定义为类变量。
实际生产环境中的服务器资产信息通常需要从资产管理系统、配置文件、云平台 API 或监控系统中获取。这里的重点是学习数据组织方式,而不是实现完整的资产管理平台。
动手练习
请你自己编写一个简单的 Server 类,完成以下要求:
- 定义类变量
count,初始值为0。 - 使用
__init__()保存服务器名称和 IP 地址。 - 每次执行初始化方法中的计数语句时,让
count增加1。 - 创建三台服务器对象。
- 分别打印三台服务器的名称。
- 最后打印累计初始化次数。
可以先尝试独立完成,再参考下面的答案。
参考答案
1 | class Server: |
运行结果:
1 | web01 |
如果你能够独立完成这个练习,并解释为什么 Server.count 是 3,而每台服务器的 name 可以不同,就已经掌握了类变量与实例变量的基本区别。
写在最后
本篇从服务器资产管理场景出发,学习了类变量,并进一步理解了 Python 的属性查找与赋值行为。
记住以下几个关键点:
- 实例变量通常通过
self.属性名设置,用于保存对象自己的数据。 - 类变量定义在类的命名空间中,可以通过类或实例访问。
- 读取属性时,如果实例自身没有对应属性,Python 通常会继续查找所属类及其继承链。
- 通过实例赋值通常是在实例自身设置或修改属性,不会自动修改同名类变量。
- 修改类变量时,优先使用
类名.变量名 = 新值。 - 累计计数不等于实时对象数量。简单的类变量计数器不会因为对象删除而自动递减。
下一篇,我们将学习 继承与多态。
当我们需要管理 Web 服务器、数据库服务器和缓存服务器时,它们可能拥有一些共同的数据和操作,也可能分别具备不同的功能。怎样减少重复代码,又能让不同类型的服务器实现各自的行为?这就是继承与多态要解决的问题。
