Python 系列(二十五) 学会模块与 import,让代码告别重复编写
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 函数,知道可以把一段需要重复使用的操作封装起来,需要时再调用。
但随着程序越来越复杂,只把代码写在一个文件中,也会遇到新的问题。
假设你是一名 Linux 运维工程师,想用 Python 编写一个服务器巡检工具。最开始,你只想检查系统负载;后来,又想检查内存、磁盘空间和服务状态。
一开始,代码只有十几行,看起来很简单。可随着功能不断增加,一个文件里可能会出现几十个函数,甚至几百行代码。
找一个函数要翻半天,修改一个功能还担心影响其他功能。更麻烦的是,如果另一个脚本也需要检查磁盘,你可能又要把相同的代码复制一遍。
有没有一种办法,可以把不同功能分别保存起来,需要时直接拿来使用?
有!这就是 Python 的模块。
今天我们不急着记概念,而是从创建文件开始,亲手把一个简单的服务器巡检程序拆分成多个文件,再一步步理解模块到底是怎么工作的。
先动手:为什么需要模块
先假设我们已经写好了一个简单的巡检程序。
创建一个名为 server_check.py 的文件,写入下面的代码:
1 | def check_cpu(): |
如果你还不熟悉函数,可以先把它理解为一段有名字、可以重复使用的代码。
例如,check_cpu() 就代表执行检查 CPU 的那段代码。
保存文件后,在终端进入文件所在的目录,执行:
1 | python server_check.py |
运行结果如下:
1 | 正在检查 CPU |
很好,我们已经有了一个简单的巡检程序。
但现在想象一下:除了这个程序,你又要写一个每天生成巡检报告的脚本。报告脚本也需要检查 CPU 和内存,难道要把这些函数重新复制一遍吗?
当然不必。
我们可以把巡检功能保存在一个文件中,让其他 Python 文件也能使用它们。
这就像把常用工具放进工具箱,需要时拿出来使用,而不是每次工作都重新制作一套工具。
在 Python 中,这个工具箱就可以是一个模块。
什么是 Python 模块
先认识一个概念:模块(Module)通常就是一个包含 Python 代码的 .py 文件。
例如:
1 | python-lab/ |
这里有两个文件:
server_check.py:保存服务器巡检功能。main.py:负责决定什么时候执行巡检。
文件名中的 .py 表示这是一个 Python 源代码文件。
当我们导入模块时,通常使用不带 .py 的名称。因此,server_check.py 对应的模块名是 server_check。
现在,我们来创建第二个文件。
在 main.py 中写入:
1 | import server_check |
这行代码的意思是:让当前程序导入 server_check 模块,以便使用其中提供的内容。
不过,只写这一行还没有主动调用我们定义的巡检函数。接下来,我们再补充几行代码。
第一次导入模块
把 main.py 修改为:
1 | import server_check |
确认两个文件保存在同一个目录中:
1 | python-lab/ |
然后执行:
1 | python main.py |
你会看到:
1 | 正在检查 CPU |
咦,为什么每项检查都执行了两次?
先别着急。这恰好是我们理解模块的第一个关键点。
第一次输出,来自 server_check.py 文件最后的三次函数调用。
当 Python 执行 import server_check 时,它不仅让主程序能够使用这个模块,还会执行模块顶层的代码。所谓顶层代码,就是没有放在函数体、条件块等内部的代码。
因此,原来的这三行代码:
1 | check_cpu() |
在模块导入时就已经执行了一次。
随后,main.py 又主动调用了一遍,所以输出重复了。
这告诉我们一件重要的事情:
导入模块时,模块顶层的可执行语句可能会立即运行,并不是只有调用函数时才会执行代码。
那么,怎么避免导入模块时自动执行整套巡检呢?
我们需要先把“定义功能”和“启动程序”分开。
把功能定义和程序启动分开
先回到 server_check.py。
把文件末尾的三行函数调用删除,只保留函数定义:
1 | def check_cpu(): |
现在,这个文件只负责定义三个函数,并没有在文件顶层主动调用它们。
再次执行:
1 | python main.py |
输出变成:
1 | 正在检查 CPU |
这次每项检查只执行了一次。
原因很简单:导入 server_check 时,Python 会创建这些函数的定义,但不会自动执行每个函数体。只有 main.py 明确调用函数时,里面的代码才会运行。
现在,我们已经把两个角色分开了:
server_check.py负责提供巡检功能。main.py负责调用这些功能。
这就是模块最基本的使用方式。
不过,新的问题又来了。
假设我们希望 server_check.py 不仅能被其他文件导入,还能像普通脚本一样直接运行,自动完成整套巡检。应该怎么办?
认识 __name__:Python 如何知道文件的运行方式
Python 为模块提供了一个特殊属性,叫作 __name__。
先不用急着记住这个名字。我们通过一个小实验来理解它。
在 server_check.py 的开头添加一行:
1 | print(__name__) |
这里的 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 | def check_cpu(): |
先看最后两行:
1 | if __name__ == "__main__": |
这里的 if 是条件判断,可以理解为“如果条件成立,就执行下面缩进的代码”。
因此,这段代码的意思是:
如果当前文件是作为主程序直接运行的,就调用 run_all_checks(),执行全部巡检。
当我们执行:
1 | python server_check.py |
输出结果是:
1 | 正在检查 CPU |
这是因为直接运行文件时,__name__ 的值为 "__main__",条件成立。
如果换成 main.py:
1 | import server_check |
运行:
1 | python main.py |
输出结果是:
1 | 主程序开始执行 |
虽然 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 | python-lab/ |
请在 Linux 系统上创建这两个文件。下面的示例使用 Python 标准库,不需要额外安装第三方软件包。
编写 server_check.py
将下面的完整代码保存为 server_check.py:
1 | import os |
这段代码看起来比之前长了很多,但不要被代码量吓到。我们主要关注它是如何组织的。
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 | ======================================== |
具体数字会根据你的 Linux 服务器配置和当前负载变化,不同机器得到不同结果是正常的。
创建主程序 main.py
接下来,我们再创建一个主程序。
在 main.py 中写入:
1 | import server_check |
运行:
1 | python main.py |
你会发现,巡检仍然能够正常执行。
输出
1 | 主程序开始执行 |
但这一次,执行过程是这样的:
- Python 开始运行
main.py。 main.py导入server_check。- Python 初始化
server_check模块,定义其中的巡检函数。 - 因为
server_check是被导入的模块,它自己的入口条件不成立,因此不会自动启动巡检。 main.py继续运行,调用main()。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 | def check_cpu(): |
这段代码只是定义函数。只有执行:
1 | check_cpu() |
函数体中的打印语句才会运行。
是不是所有代码都必须写在 if __name__ == "__main__": 中?
不是。
可复用的函数和类通常直接定义在模块中。入口条件块一般用于放置启动程序时才需要执行的逻辑,例如调用主函数、解析命令行参数等。
写在最后
这篇文章,我们从一个简单的服务器巡检脚本出发,逐步学习了 Python 模块的概念、导入方式,以及为什么模块顶层的代码可能在导入时执行。
接着,我们通过 __name__ 理解了直接运行与导入模块之间的区别,最后把这些知识应用到了一个能够读取 Linux 系统信息的巡检工具中。
记住两个核心要点:
- 模块让代码可以拆分和复用。 一个文件负责一类功能,其他文件通过导入就可以使用。
if __name__ == "__main__":用来控制独立运行入口。 它让同一个文件既能直接运行,又能被其他程序导入复用。
刚开始接触编程时,不需要一次记住所有模块机制。先亲手创建两个文件,运行几次,观察输出有什么变化,再回头理解原理,会更容易掌握。
下一篇,我们将继续学习如何从其他模块导入函数、类和变量,并逐步把多个 Python 文件组织成结构更清晰的小项目。
