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,让代码告别重复编写

前面我们学习了 Python 函数,知道可以把一段需要重复使用的操作封装起来,需要时再调用。

但随着程序越来越复杂,只把代码写在一个文件中,也会遇到新的问题。

假设你是一名 Linux 运维工程师,想用 Python 编写一个服务器巡检工具。最开始,你只想检查系统负载;后来,又想检查内存、磁盘空间和服务状态。

一开始,代码只有十几行,看起来很简单。可随着功能不断增加,一个文件里可能会出现几十个函数,甚至几百行代码。

找一个函数要翻半天,修改一个功能还担心影响其他功能。更麻烦的是,如果另一个脚本也需要检查磁盘,你可能又要把相同的代码复制一遍。

有没有一种办法,可以把不同功能分别保存起来,需要时直接拿来使用?

有!这就是 Python 的模块。

今天我们不急着记概念,而是从创建文件开始,亲手把一个简单的服务器巡检程序拆分成多个文件,再一步步理解模块到底是怎么工作的。

先动手:为什么需要模块

先假设我们已经写好了一个简单的巡检程序。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def check_cpu():
print("正在检查 CPU")


def check_memory():
print("正在检查内存")


def check_disk():
print("正在检查磁盘")


check_cpu()
check_memory()
check_disk()

如果你还不熟悉函数,可以先把它理解为一段有名字、可以重复使用的代码。

例如,check_cpu() 就代表执行检查 CPU 的那段代码。

保存文件后,在终端进入文件所在的目录,执行:

1
python server_check.py

运行结果如下:

1
2
3
正在检查 CPU
正在检查内存
正在检查磁盘

很好,我们已经有了一个简单的巡检程序。

但现在想象一下:除了这个程序,你又要写一个每天生成巡检报告的脚本。报告脚本也需要检查 CPU 和内存,难道要把这些函数重新复制一遍吗?

当然不必。

我们可以把巡检功能保存在一个文件中,让其他 Python 文件也能使用它们。

这就像把常用工具放进工具箱,需要时拿出来使用,而不是每次工作都重新制作一套工具。

在 Python 中,这个工具箱就可以是一个模块。

什么是 Python 模块

先认识一个概念:模块(Module)通常就是一个包含 Python 代码的 .py 文件。

例如:

1
2
3
python-lab/
├── server_check.py
└── main.py

这里有两个文件:

  • server_check.py:保存服务器巡检功能。
  • main.py:负责决定什么时候执行巡检。

文件名中的 .py 表示这是一个 Python 源代码文件。

当我们导入模块时,通常使用不带 .py 的名称。因此,server_check.py 对应的模块名是 server_check。

现在,我们来创建第二个文件。

在 main.py 中写入:

1
import server_check

这行代码的意思是:让当前程序导入 server_check 模块,以便使用其中提供的内容。

不过,只写这一行还没有主动调用我们定义的巡检函数。接下来,我们再补充几行代码。

第一次导入模块

把 main.py 修改为:

1
2
3
4
5
import server_check

server_check.check_cpu()
server_check.check_memory()
server_check.check_disk()

确认两个文件保存在同一个目录中:

1
2
3
python-lab/
├── server_check.py
└── main.py

然后执行:

1
python main.py

你会看到:

1
2
3
4
5
6
正在检查 CPU
正在检查内存
正在检查磁盘
正在检查 CPU
正在检查内存
正在检查磁盘

咦,为什么每项检查都执行了两次?

先别着急。这恰好是我们理解模块的第一个关键点。

第一次输出,来自 server_check.py 文件最后的三次函数调用。

当 Python 执行 import server_check 时,它不仅让主程序能够使用这个模块,还会执行模块顶层的代码。所谓顶层代码,就是没有放在函数体、条件块等内部的代码。

因此,原来的这三行代码:

1
2
3
check_cpu()
check_memory()
check_disk()

在模块导入时就已经执行了一次。

随后,main.py 又主动调用了一遍,所以输出重复了。

这告诉我们一件重要的事情:

导入模块时,模块顶层的可执行语句可能会立即运行,并不是只有调用函数时才会执行代码。

那么,怎么避免导入模块时自动执行整套巡检呢?

我们需要先把“定义功能”和“启动程序”分开。

把功能定义和程序启动分开

先回到 server_check.py。

把文件末尾的三行函数调用删除,只保留函数定义:

1
2
3
4
5
6
7
8
9
10
def check_cpu():
print("正在检查 CPU")


def check_memory():
print("正在检查内存")


def check_disk():
print("正在检查磁盘")

现在,这个文件只负责定义三个函数,并没有在文件顶层主动调用它们。

再次执行:

1
python main.py

输出变成:

1
2
3
正在检查 CPU
正在检查内存
正在检查磁盘

这次每项检查只执行了一次。

原因很简单:导入 server_check 时,Python 会创建这些函数的定义,但不会自动执行每个函数体。只有 main.py 明确调用函数时,里面的代码才会运行。

现在,我们已经把两个角色分开了:

  • server_check.py 负责提供巡检功能。
  • main.py 负责调用这些功能。

这就是模块最基本的使用方式。

不过,新的问题又来了。

假设我们希望 server_check.py 不仅能被其他文件导入,还能像普通脚本一样直接运行,自动完成整套巡检。应该怎么办?

认识 __name__:Python 如何知道文件的运行方式

Python 为模块提供了一个特殊属性,叫作 __name__。

先不用急着记住这个名字。我们通过一个小实验来理解它。

在 server_check.py 的开头添加一行:

1
2
3
4
5
print(__name__)


def check_cpu():
print("正在检查 CPU")

这里的 print() 会输出变量 __name__ 的值。

变量可以先理解为一个用名字保存信息的对象。这里的 __name__ 是 Python 提供的特殊属性,不需要我们自己创建。

现在,直接运行这个文件:

1
python server_check.py

输出结果是:

1
__main__

接着,在 main.py 中写入:

1
import server_check

执行:

1
python main.py

这次输出结果是:

1
server_check

同样是 server_check.py,为什么两次输出不同?

因为 Python 会根据模块的使用方式设置 __name__ 的值。

使用方式__name__ 的常见值
直接运行 python server_check.py__main__
通过 import server_check 导入server_check

当我们直接运行一个 Python 文件时,Python 会把它作为当前程序的入口,通常将它的 __name__ 设置为 "__main__"。

而当其他文件导入 server_check 时,这个模块的名称通常就是 "server_check"。

于是,我们就可以根据 __name__ 的值,判断代码是否处于直接运行的场景。

理解 if __name__ == "__main__":

现在,我们终于可以解决最开始的问题了。

我们希望 server_check.py 同时具备两种能力:

  • 被其他程序导入时,提供巡检函数。
  • 直接运行时,自动执行整套巡检。

可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def check_cpu():
print("正在检查 CPU")


def check_memory():
print("正在检查内存")


def check_disk():
print("正在检查磁盘")


def run_all_checks():
check_cpu()
check_memory()
check_disk()


if __name__ == "__main__":
run_all_checks()

先看最后两行:

1
2
if __name__ == "__main__":
run_all_checks()

这里的 if 是条件判断,可以理解为“如果条件成立,就执行下面缩进的代码”。

因此,这段代码的意思是:

如果当前文件是作为主程序直接运行的,就调用 run_all_checks(),执行全部巡检。

当我们执行:

1
python server_check.py

输出结果是:

1
2
3
正在检查 CPU
正在检查内存
正在检查磁盘

这是因为直接运行文件时,__name__ 的值为 "__main__",条件成立。

如果换成 main.py:

1
2
3
4
import server_check

print("主程序开始执行")
server_check.check_cpu()

运行:

1
python main.py

输出结果是:

1
2
主程序开始执行
正在检查 CPU

虽然 server_check.py 被导入了,但其中 if __name__ == "__main__": 条件块里的 run_all_checks() 并没有自动执行。

原因是导入时,server_check.__name__ 通常是 "server_check",不等于 "__main__"。

但 main.py 仍然可以主动调用 server_check.check_cpu(),因为这个函数已经定义在模块中。

这就是这条语句最重要的用途:让同一个文件既能独立运行,也能作为模块供其他程序复用。

用流程图把整个过程串起来

我们把刚才学到的内容整理一下。

flowchart TD
    A["运行 Python 文件"] --> B{"直接运行还是导入?"}
    B -->|直接运行| C["__name__ = '__main__'"]
    B -->|被其他文件导入| D["__name__ 通常为模块名"]
    C --> E["执行模块顶层代码"]
    D --> E
    E --> F{"是否满足入口条件?"}
    F -->|是| G["执行 run_all_checks()"]
    F -->|否| H["跳过入口条件块"]
    G --> I["巡检执行完成"]
    H --> J["等待导入方调用函数"]

这里需要特别注意:不管是直接运行还是导入,模块顶层的普通语句都会在模块初始化过程中执行。条件块只决定其中的代码是否执行,并不会阻止整个模块被加载。

还有一个细节:同一个 Python 进程中,模块通常只在首次导入时执行初始化代码。之后再次导入时,Python 通常会复用已经缓存的模块,而不会重新执行全部顶层代码。

初学阶段先掌握前面的运行规则即可,不必急着深入模块缓存机制。

Linux 运维实战:编写一个真正能检查系统的模块

前面我们用 print() 模拟了巡检。接下来,把这个例子向前推进一步:让 Python 真正读取 Linux 系统信息。

我们先不追求复杂的告警系统,只完成三个任务:

  • 获取系统负载。
  • 读取内存信息。
  • 检查根文件系统的磁盘空间。

最终项目结构如下:

1
2
3
python-lab/
├── server_check.py
└── main.py

请在 Linux 系统上创建这两个文件。下面的示例使用 Python 标准库,不需要额外安装第三方软件包。

编写 server_check.py

将下面的完整代码保存为 server_check.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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import os
import shutil
from pathlib import Path


def check_load():
"""检查 Linux 系统负载。"""
print("\n[系统负载]")

if not hasattr(os, "getloadavg"):
print("当前平台不支持读取系统负载")
return

load1, load5, load15 = os.getloadavg()

print(f"1 分钟负载:{load1:.2f}")
print(f"5 分钟负载:{load5:.2f}")
print(f"15 分钟负载:{load15:.2f}")
print(f"逻辑 CPU 数量:{os.cpu_count()}")


def check_memory():
"""检查 Linux 内存信息。"""
print("\n[内存使用情况]")

meminfo = Path("/proc/meminfo")

if not meminfo.is_file():
print("未找到 /proc/meminfo,无法读取内存信息")
return

try:
values = {}

for line in meminfo.read_text().splitlines():
key, value = line.split(":", 1)
values[key] = int(value.strip().split()[0])

total = values["MemTotal"]
available = values["MemAvailable"]
used = total - available
usage = used / total * 100 if total else 0

print(f"内存总量:{total / 1024:.2f} MiB")
print(f"可用内存:{available / 1024:.2f} MiB")
print(f"估算已用:{used / 1024:.2f} MiB")
print(f"估算使用率:{usage:.1f}%")

except (OSError, KeyError, ValueError, IndexError) as exc:
print(f"读取内存信息失败:{exc}")


def check_disk(path="/"):
"""检查指定路径所在文件系统的使用情况。"""
print(f"\n[磁盘使用情况:{path}]")

try:
usage = shutil.disk_usage(path)
except OSError as exc:
print(f"读取磁盘使用情况失败:{exc}")
return

used_percent = usage.used / usage.total * 100 if usage.total else 0

print(f"总容量:{usage.total / 1024**3:.2f} GiB")
print(f"已使用:{usage.used / 1024**3:.2f} GiB")
print(f"可用空间:{usage.free / 1024**3:.2f} GiB")
print(f"已用比例:{used_percent:.1f}%")


def run_all_checks():
"""执行全部巡检项目。"""
print("=" * 40)
print("Linux 服务器巡检开始")
print("=" * 40)

check_load()
check_memory()
check_disk("/")

print("\n巡检完成")


if __name__ == "__main__":
run_all_checks()

这段代码看起来比之前长了很多,但不要被代码量吓到。我们主要关注它是如何组织的。

check_load() 负责检查系统负载,check_memory() 负责检查内存,check_disk() 负责检查磁盘。每个函数各自完成一项任务。

run_all_checks() 则负责把这些功能组织起来,依次执行。

最后的 if __name__ == "__main__": 负责决定:直接运行这个文件时,是否自动启动整套巡检。

这里还使用了几项标准库功能:

接口作用
os.getloadavg()获取系统 1、5、15 分钟负载,具体取决于平台是否支持
os.cpu_count()获取逻辑 CPU 数量
/proc/meminfo在 Linux 中读取内存统计信息
shutil.disk_usage()获取指定路径所在文件系统的容量信息
Path帮助我们处理文件路径

内存使用率使用 MemTotal - MemAvailable 进行估算,比简单地用总内存减去 MemFree 更能反映 Linux 系统中缓存等因素的影响。

另外,系统负载不等于 CPU 使用率。负载还会受到可运行任务和不可中断睡眠任务等因素影响,不能仅凭负载数字判断 CPU 使用率。

这份代码用于学习模块组织和基础巡检,并不是完整的生产级监控系统。

直接运行巡检模块

现在,在 Linux 终端进入 server_check.py 所在目录,执行:

1
python server_check.py

程序会直接执行 run_all_checks(),输出当前系统的负载、内存和根文件系统的使用情况。

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
========================================
Linux 服务器巡检开始
========================================

[系统负载]
1 分钟负载:0.32
5 分钟负载:0.28
15 分钟负载:0.21
逻辑 CPU 数量:4

[内存使用情况]
内存总量:7980.00 MiB
可用内存:5000.00 MiB
估算已用:2980.00 MiB
估算使用率:37.34%

[磁盘使用情况:/]
总容量:200.00 GiB
已使用:67.39 GiB
可用空间:132.61 GiB
已用比例:33.7%

巡检完成

具体数字会根据你的 Linux 服务器配置和当前负载变化,不同机器得到不同结果是正常的。

创建主程序 main.py

接下来,我们再创建一个主程序。

在 main.py 中写入:

1
2
3
4
5
6
7
8
9
10
import server_check


def main():
print("主程序开始执行")
server_check.run_all_checks()


if __name__ == "__main__":
main()

运行:

1
python main.py

你会发现,巡检仍然能够正常执行。

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
主程序开始执行
========================================
Linux 服务器巡检开始
========================================

[系统负载]
当前平台不支持读取系统负载

[内存使用情况]
未找到 /proc/meminfo,无法读取内存信息

[磁盘使用情况:/]
总容量:200.00 GiB
已使用:67.39 GiB
可用空间:132.61 GiB
已用比例:33.7%

巡检完成

但这一次,执行过程是这样的:

  1. Python 开始运行 main.py。
  2. main.py 导入 server_check。
  3. Python 初始化 server_check 模块,定义其中的巡检函数。
  4. 因为 server_check 是被导入的模块,它自己的入口条件不成立,因此不会自动启动巡检。
  5. main.py 继续运行,调用 main()。
  6. main() 主动调用 server_check.run_all_checks(),巡检才正式开始。

这就把整个过程串起来了。

我们既可以直接运行 server_check.py 完成巡检,也可以在其他程序中导入它,只使用需要的功能。

如果未来还要生成巡检报告,可以继续创建 report.py;如果要接入云平台 API,也可以再增加相应模块。不同功能各自维护,主程序负责组织执行流程,整个项目就更容易扩展。

初学者容易遇到的几个问题

为什么提示找不到 server_check 模块?

最常见的原因是文件不在 Python 能够找到的位置。

请先确认 main.py 和 server_check.py 放在同一个目录下,并在这个目录中执行:

1
python main.py

还要注意,文件实际名称必须是 server_check.py,不要不小心保存成 server_check.py.txt。

为什么导入模块时,某些代码还是执行了?

因为模块顶层的普通语句会在模块初始化时执行。if __name__ == "__main__": 只控制条件块里的代码,不会阻止模块中的其他顶层语句执行。

为什么函数定义了,却没有输出?

定义函数并不等于执行函数。

例如:

1
2
def check_cpu():
print("正在检查 CPU")

这段代码只是定义函数。只有执行:

1
check_cpu()

函数体中的打印语句才会运行。

是不是所有代码都必须写在 if __name__ == "__main__": 中?

不是。

可复用的函数和类通常直接定义在模块中。入口条件块一般用于放置启动程序时才需要执行的逻辑,例如调用主函数、解析命令行参数等。

写在最后

这篇文章,我们从一个简单的服务器巡检脚本出发,逐步学习了 Python 模块的概念、导入方式,以及为什么模块顶层的代码可能在导入时执行。

接着,我们通过 __name__ 理解了直接运行与导入模块之间的区别,最后把这些知识应用到了一个能够读取 Linux 系统信息的巡检工具中。

记住两个核心要点:

  • 模块让代码可以拆分和复用。 一个文件负责一类功能,其他文件通过导入就可以使用。
  • if __name__ == "__main__": 用来控制独立运行入口。 它让同一个文件既能直接运行,又能被其他程序导入复用。

刚开始接触编程时,不需要一次记住所有模块机制。先亲手创建两个文件,运行几次,观察输出有什么变化,再回头理解原理,会更容易掌握。

下一篇,我们将继续学习如何从其他模块导入函数、类和变量,并逐步把多个 Python 文件组织成结构更清晰的小项目。