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 到底是什么?一文搞懂常见异常

在 Linux 运维工作中,我们可能需要使用 Python 管理几十台甚至几百台服务器。

如果每创建一个服务器对象,就想记录累计创建了多少个对象,应该怎么做?

是给每台服务器单独保存一个计数值,还是让所有服务器共同使用一个计数值?

这正是 Python 类变量要解决的问题。本篇从服务器资产管理的实际场景出发,带你理解实例变量与类变量的区别,掌握类变量的定义、访问和修改方式。

从实际运维问题开始

假设我们正在编写一个简单的服务器资产管理程序,需要记录以下服务器信息:

服务器名称IP 地址用途状态
web01192.168.1.10Web 服务器running
web02192.168.1.11Web 服务器running
db01192.168.1.20数据库服务器stopped

前面学习类和对象时,我们已经知道,可以定义一个 Server 类,再根据这个类创建多个服务器对象。

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


web01 = Server("web01", "192.168.1.10")
web02 = Server("web02", "192.168.1.11")
db01 = Server("db01", "192.168.1.20")

print(web01.name)
print(web02.name)
print(db01.name)

运行结果:

1
2
3
web01
web02
db01

这段代码中,我们创建了三个 Server 对象,每个对象都保存了自己的服务器名称和 IP 地址。

例如,web01.name 保存的是 web01,而 db01.name 保存的是 db01。

这些数据属于各自的对象,因此称为实例变量。

不过,现在出现了一个新需求:我们希望程序能够记录累计创建了多少个服务器对象。

创建第一个对象,计数变成 1;创建第二个对象,计数变成 2;创建第三个对象,计数变成 3。

这个计数值与某一台具体的服务器没有直接关系,而是与整个 Server 类的对象初始化过程有关。

能不能让多个服务器对象共同使用同一个计数值?

要解决这个问题,就需要认识类变量。

为什么实例变量不适合直接完成共享计数

先看一种可能的写法:

1
2
3
4
5
class Server:
def __init__(self, name, ip):
self.name = name
self.ip = ip
self.count = 0

这里的 self.count = 0 是什么意思?

还记得 self 表示什么吗?它表示当前正在初始化的对象。

因此,创建 web01 时,Python 会在 web01 对象上设置一个 count 属性;创建 web02 时,又会在 web02 对象上设置一个独立的 count 属性。

每个对象的 count 都从 0 开始,互不影响。

这正是实例变量的特点:每个对象都可以保存自己的数据。

但我们想要的是一个统一的计数值,而不是每台服务器各自保存一个互不相关的计数器。

接下来,我们把计数值放到类中。

在类中定义变量

Python 不仅允许我们通过 self.属性名 为对象保存数据,也允许直接在类的代码块中定义变量。

例如:

1
2
3
4
5
6
class Server:
count = 0

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

注意这行代码:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Server:
count = 0

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

Server.count += 1


web01 = Server("web01", "192.168.1.10")
web02 = Server("web02", "192.168.1.11")
db01 = Server("db01", "192.168.1.20")

print(Server.count)

运行结果:

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
2
print(web01.count)
print(web02.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
2
3
4
5
6
7
8
9
print(Server.count)
print(web01.count)
print(web02.count)

web01.count = 100

print(Server.count)
print(web01.count)
print(web02.count)

运行结果:

1
2
3
4
5
6
3
3
3
3
100
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
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:
count = 0

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

Server.count += 1

def __str__(self):
return f"{self.name} | {self.ip} | {self.status}"


web01 = Server("web01", "192.168.1.10", "running")
web02 = Server("web02", "192.168.1.11", "running")
db01 = Server("db01", "192.168.1.20", "stopped")

servers = [web01, web02, db01]

for server in servers:
print(server)

print(f"累计初始化次数:{Server.count}")

运行结果:

1
2
3
4
web01 | 192.168.1.10 | running
web02 | 192.168.1.11 | running
db01 | 192.168.1.20 | stopped
累计初始化次数:3

这里整合了前面学习过的知识:

  • Server 类定义服务器对象的数据和行为。
  • __init__() 初始化每台服务器自己的名称、IP 地址和状态。
  • Server.count 累计记录计数语句执行的次数。
  • __str__() 定义服务器对象转换成字符串时的显示格式。
  • 列表保存多个服务器对象,for 循环逐个输出服务器信息。

在这个例子中,名称、IP 地址和状态都属于具体服务器,因此保存在实例变量中;累计计数值则定义为类变量。

实际生产环境中的服务器资产信息通常需要从资产管理系统、配置文件、云平台 API 或监控系统中获取。这里的重点是学习数据组织方式,而不是实现完整的资产管理平台。

动手练习

请你自己编写一个简单的 Server 类,完成以下要求:

  • 定义类变量 count,初始值为 0。
  • 使用 __init__() 保存服务器名称和 IP 地址。
  • 每次执行初始化方法中的计数语句时,让 count 增加 1。
  • 创建三台服务器对象。
  • 分别打印三台服务器的名称。
  • 最后打印累计初始化次数。

可以先尝试独立完成,再参考下面的答案。

参考答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Server:
count = 0

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

Server.count += 1


web01 = Server("web01", "192.168.1.10")
web02 = Server("web02", "192.168.1.11")
db01 = Server("db01", "192.168.1.20")

print(web01.name)
print(web02.name)
print(db01.name)

print(f"累计初始化次数:{Server.count}")

运行结果:

1
2
3
4
web01
web02
db01
累计初始化次数:3

如果你能够独立完成这个练习,并解释为什么 Server.count 是 3,而每台服务器的 name 可以不同,就已经掌握了类变量与实例变量的基本区别。

写在最后

本篇从服务器资产管理场景出发,学习了类变量,并进一步理解了 Python 的属性查找与赋值行为。

记住以下几个关键点:

  • 实例变量通常通过 self.属性名 设置,用于保存对象自己的数据。
  • 类变量定义在类的命名空间中,可以通过类或实例访问。
  • 读取属性时,如果实例自身没有对应属性,Python 通常会继续查找所属类及其继承链。
  • 通过实例赋值通常是在实例自身设置或修改属性,不会自动修改同名类变量。
  • 修改类变量时,优先使用 类名.变量名 = 新值。
  • 累计计数不等于实时对象数量。简单的类变量计数器不会因为对象删除而自动递减。

下一篇,我们将学习 继承与多态。

当我们需要管理 Web 服务器、数据库服务器和缓存服务器时,它们可能拥有一些共同的数据和操作,也可能分别具备不同的功能。怎样减少重复代码,又能让不同类型的服务器实现各自的行为?这就是继承与多态要解决的问题。