Python 系列(二十六) 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 系列(二十六) Python 模块入门,从 import 到模块复用
- Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
- Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
- Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
- Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
- Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
前面我们学习了 Python 的变量、数据类型、条件判断、循环以及函数。
随着程序越来越复杂,我们可能会遇到一个问题:所有代码都写在同一个 Python 文件中,会不会越来越难维护?
比如,我们想写一个服务器巡检工具,既要检查磁盘使用率,又要检查内存、CPU 和服务状态。如果把所有功能都放进一个文件,代码很快就会变得臃肿。
有没有办法把不同功能拆分到不同文件中,需要时再把它们组合起来?
当然有,这就是 Python 的模块(Module)。
这篇文章,我们从最基础的
import开始,逐步理解模块的导入方式、执行顺序、缓存机制和搜索路径,最后用一个简单的服务器磁盘检查项目把这些知识串起来。
为什么需要模块?
先从一个简单的例子开始。
假设我们要编写一个程序,计算服务器磁盘的使用率:
1 | total = 1000 |
运行后,输出如下:
1 | 磁盘使用率:76.0% |
这段代码很简单,但如果后续还要检查内存、CPU 和服务状态,所有代码都堆在一起,就不容易维护了。
我们可以按照功能拆分文件:
1 | server_check/ |
每个文件负责一项工作:
main.py:程序入口,负责组织整个巡检流程。disk_check.py:检查磁盘使用情况。memory_check.py:检查内存使用情况。service_check.py:检查服务运行状态。
这些 .py 文件就可以作为 Python 模块使用。
简单理解:一个 Python 模块通常就是一个 .py 文件。 我们可以把相关代码放在不同模块中,再通过导入的方式使用它们。
这样做的好处很直接:代码更容易复用,功能划分更清晰,也方便后续维护和测试。
使用 import 导入模块
先学习最常见的导入方式:import。
Python 自带了许多可以直接使用的功能。例如,math 模块提供了平方根、圆周率等数学功能。
创建第一个示例
为了避免不同示例之间相互覆盖,我们为每个练习单独创建一个目录。
先创建目录:
1 | mkdir -p python_module_demo/01_basic |
在这个目录下创建 math_demo.py,写入以下代码:
1 | import math |
这里有几个值得注意的地方。
import math 表示导入 math 模块。
math.sqrt(16) 表示调用 math 模块中的 sqrt() 函数,计算 16 的平方根。
math.pi 表示访问 math 模块中的 pi 属性,也就是圆周率。
注意,导入模块后,访问其中的函数或属性时,需要使用模块名加点号的形式。
运行程序
在当前目录中执行:
1 | python math_demo.py |
输出类似:
1 | 4.0 |
为什么结果是 4.0,而不是 4?
因为 math.sqrt() 返回的是浮点数。即使平方根恰好是整数,结果仍然以浮点数的形式表示。
这里的 python math_demo.py 表示使用 Python 解释器执行当前目录下的 math_demo.py 文件。
如果系统使用的是 python3 命令,也可以执行:
1 | python3 math_demo.py |
import 的基本语法
1 | import 模块名 |
导入后,可以通过下面的方式访问模块中的内容:
1 | 模块名.函数名() |
例如:
1 | import math |
这种写法的优点是清晰:看到 math.sqrt(),就知道这个函数来自 math 模块。
使用 from … import 导入指定内容
刚才我们导入了整个 math 模块,再通过 math.sqrt() 调用函数。
如果只需要其中的 sqrt() 函数,能不能直接导入它?
可以,这时就可以使用 from ... import ... 语法。
创建独立示例
创建另一个目录:
1 | mkdir -p ../02_from_import |
创建 sqrt_demo.py,写入以下代码:
1 | from math import sqrt, pi |
这里的含义是:
from math:指定从哪个模块导入。import sqrt, pi:指定要导入的函数和属性。
导入之后,可以直接使用 sqrt() 和 pi,不必再写 math.sqrt() 和 math.pi。
运行程序:
1 | python sqrt_demo.py |
输出:
1 | 4.0 |
两种导入方式有什么区别?
| 导入方式 | 调用方式 | 特点 |
|---|---|---|
import math | math.sqrt(16) | 来源清晰,适合导入模块后使用多个功能 |
from math import sqrt | sqrt(16) | 调用简洁,适合明确使用少量功能 |
这两种方式都很常见,可以根据实际需求选择。
需要注意的是,from math import sqrt 并不意味着 Python 只加载了 sqrt() 这一个独立文件。它仍然需要导入对应的 math 模块,只是将指定的名称直接绑定到当前命名空间中。
初学阶段,可以优先使用 import 模块名。当需要使用的内容比较明确时,再使用 from ... import ...。
使用 as 为模块起别名
有时,模块名称比较长,或者我们希望在代码中使用更简短的名称,就可以使用 as 设置别名。
创建目录:
1 | mkdir -p ../03_alias |
创建 alias_demo.py:
1 | import math as m |
运行:
1 | python alias_demo.py |
输出:
1 | 6.0 |
这里的 as m 表示将导入的 math 模块绑定为名称 m。后续就可以使用 m.sqrt() 和 m.pi。
同样,from ... import ... 也可以设置别名:
1 | from math import sqrt as square_root |
输出:
1 | 7.0 |
别名只是当前代码中的另一个名称,并不会改变模块本身。
实际开发中,很多常用库都有约定俗成的别名。例如:
1 | import numpy as np |
不过,刚开始学习时,不必为了缩短代码而给所有模块起别名。名称清晰通常比名称简短更重要。
Python 模块有哪些类型?
前面使用的 math 是 Python 自带的模块。但在查阅资料时,你可能还会看到“内置模块”“标准库模块”和“第三方模块”等说法。
这些概念有关联,但不能完全画等号。
内置模块
内置模块(Built-in Module) 是指由当前 Python 解释器以内置方式提供的模块。具体有哪些模块,与 Python 的实现和构建方式有关。
例如,在常见的 CPython 环境中,sys 就是内置模块。
它提供了与 Python 解释器运行环境相关的功能,例如查看解释器版本、访问模块搜索路径等。
1 | import sys |
其中:
sys.version:查看当前 Python 解释器的版本信息。sys.builtin_module_names:查看当前解释器中以内置模块形式提供的模块名称。
1 | 3.15.0 (tags/v3.15.0:d220409, Oct 9 2026, 11:55:56) [MSC v.1951 64 bit (AMD64)] |
在常见的 CPython 环境中,列表中会包含 sys。不同 Python 实现或构建环境的结果可能不同。
Python 标准库模块
标准库(Standard Library) 是 Python 随附的一整套模块和包,提供文件操作、路径处理、数学计算、日期时间处理等常用功能。
标准库的范围比内置模块更广,其中既包括内置模块,也包括通过其他方式实现的模块。
例如:
sys:解释器运行环境相关功能,在常见 CPython 中属于内置模块。math:数学运算功能,在常见 CPython 中通常是标准库中的扩展模块。pathlib:面向对象的路径处理功能。shutil:文件和目录的高级操作。
这些标准库模块通常不需要使用 pip 单独安装,安装好 Python 后就可以使用。
例如,使用 pathlib 查看当前目录:
1 | from pathlib import Path |
输出会根据你运行程序时所在的目录而变化。
第三方模块和包
除了 Python 自带的标准库,开发者还可以安装第三方库,为程序增加更多功能。
例如:
requests:常用于 HTTP 请求。numpy:常用于数值计算。pandas:常用于数据处理。
以 requests 为例,在具备网络连接的环境中,可以使用以下命令安装:
1 | python -m pip install requests |
然后在程序中导入:
1 | import requests |
注意:这里的 requests 是第三方库,并不是 Python 默认自带的标准库。
在企业环境中,安装第三方依赖前,还需要考虑网络访问、软件源、版本兼容和安全管理等问题。
简单归纳
| 类型 | 含义 | 示例 |
|---|---|---|
| 内置模块 | 由解释器以内置方式提供的模块 | 常见 CPython 中的 sys |
| 标准库模块 | Python 随附的模块和包,范围更广 | math、pathlib、shutil |
| 第三方模块或包 | 由外部项目提供,通常需要额外安装 | requests、numpy |
这里要记住:内置模块是更具体的分类,标准库的范围更广;第三方库则来自 Python 自带标准库之外的项目。 不要把所有标准库模块都称为内置模块。
模块导入时,代码会执行吗?
这是学习模块时非常重要的一点。
我们通常认为,导入模块只是让程序能够使用其中的函数。但实际上,模块第一次被导入时,模块顶层的代码也会执行。
先通过一个实验观察。
创建模块文件
创建目录:
1 | mkdir -p ../04_import_order |
创建 order_demo.py:
1 | print("正在加载 order_demo 模块") |
输出
1 | 正在加载 order_demo 模块 |
这个文件包含三部分内容:
- 两条位于顶层的
print()语句。 - 一个变量
server_name。 - 一个函数
check_server()。
注意,def check_server(): 只是定义函数,并不会立即执行函数体中的 print()。
创建调用文件
在同一个目录下创建 main.py:
1 | print("程序开始") |
现在目录结构如下:
1 | 04_import_order/ |
运行:
1 | python main.py |
输出:
1 | 程序开始 |
我们逐行分析执行顺序。
- Python 从
main.py开始执行,因此先输出“程序开始”。 - 执行
import order_demo,Python 查找并加载order_demo模块。 - 模块首次导入时,
order_demo.py中的顶层代码按顺序执行,因此输出两条提示信息。 - 函数
check_server()被定义,但此时函数体还没有执行。 - 模块导入完成,程序继续执行
main.py后面的代码。 - 调用
order_demo.check_server(),这时才执行函数体。 - 最后输出“程序结束”。
可以把整个过程理解为:
flowchart LR
A["运行 main.py"] --> B["执行程序开始"]
B --> C["导入 order_demo"]
C --> D["执行模块顶层代码"]
D --> E["完成模块导入"]
E --> F["继续执行 main.py"]
F --> G["调用 check_server()"]
G --> H["执行函数体"]
H --> I["程序结束"]这也解释了为什么不应该随意在模块顶层执行有明显副作用的操作。例如,一导入模块就重启服务、删除文件或者执行大规模巡检,可能会让其他程序在导入时意外触发这些操作。
为什么常见 Python 文件都有 if name == “main“?
在 Python 代码中,你经常会看到下面这种写法:
1 | if __name__ == "__main__": |
这段代码有什么用?
我们继续在 04_import_order 目录中创建一个新文件 entry_demo.py:
1 | def main(): |
当我们直接运行这个文件:
1 | python entry_demo.py |
输出:
1 | 开始执行主程序 |
这是因为直接运行文件时,Python 会将该模块的 __name__ 设置为 "__main__",因此条件成立,main() 被调用。
如果另一个文件导入 entry_demo:
1 | import entry_demo |
保存为同目录下的 import_entry_demo.py,执行:
1 | python import_entry_demo.py |
输出:
1 | 导入完成 |
此时,entry_demo 是被导入的模块,其 __name__ 通常是 "entry_demo",而不是 "__main__",所以条件不成立,main() 不会因为导入而自动执行。
这是一种很常见的代码组织方式:把可复用的函数定义放在模块中,把只应该在直接运行文件时执行的逻辑放进 if __name__ == "__main__": 中。
为什么重复 import 通常不会重复执行模块代码?
假设我们在同一个程序里,多次导入同一个模块:
1 | import order_demo |
是不是会把 order_demo.py 的代码执行三遍?
通常不会。
Python 会在导入过程中缓存已经加载的模块。模块通常会被记录在 sys.modules 中;后续在同一个解释器进程中再次导入该模块时,Python 会优先复用已经缓存的模块对象,而不是重新执行整个模块。
验证模块缓存
在 04_import_order 目录下创建 cache_demo.py:
1 | print("cache_demo 模块正在执行") |
再创建 test_cache.py:
1 | import cache_demo |
运行:
1 | python test_cache.py |
输出:
1 | cache_demo 模块正在执行 |
虽然写了三次 import cache_demo,但模块顶层的提示信息只出现一次。
原因是:第一次导入时,模块被加载并执行;后续导入则复用缓存中的模块对象。
注意,这里的“一次”是指同一个 Python 解释器进程中,在模块没有被重新加载等特殊操作的情况下。如果重新启动 Python 程序,就会创建新的进程,模块缓存也会重新建立。
sys.modules 是什么?
sys.modules 是 Python 用来记录已导入模块的字典。
我们可以通过下面的代码检查模块是否已经加载:
1 | import sys |
输出通常是:
1 | True |
这是因为前面已经导入了 math,当前解释器中存在对应的模块缓存条目。
模块导入的简化流程如下:
flowchart TD
A["执行 import 模块名"] --> B{"模块是否已在 sys.modules 中?"}
B -->|是| C["复用已缓存的模块对象"]
B -->|否| D["查找并创建模块对象"]
D --> E["将模块加入 sys.modules"]
E --> F["执行模块代码"]
C --> G["完成导入"]
F --> G这是一种便于理解的简化流程。实际导入过程还涉及模块查找器、加载器、包结构等机制;尤其是模块初始化时,Python 通常会在执行模块代码之前就将模块放入 sys.modules,这也有助于处理循环导入等情况。
如果确实需要重新执行模块怎么办?
Python 提供了 importlib.reload(),可以重新加载已经导入的模块。
例如:
1 | import importlib |
如果 cache_demo.py 中有顶层的 print(),重新加载时通常会再次看到该提示信息。
不过,重新加载模块并不意味着程序里所有已经引用的对象都会自动更新。例如,之前通过 from cache_demo import some_function 导入的名称,不会因为重新加载模块就自动重新绑定到新的函数对象。
因此,日常开发中一般不需要手动重新加载模块。修改代码后,重新运行程序往往更简单,也更容易理解。
Python 到哪里查找模块?
前面所有示例都把相关文件放在同一个目录中,所以导入过程比较顺利。
但如果模块放在其他目录,Python 是如何找到它的?
这就涉及模块搜索路径。
查看 sys.path
创建目录:
1 | mkdir -p ../06_module_path |
创建 show_path.py:
1 | import sys |
运行:
1 | python show_path.py |
输出会根据你的 Python 安装方式和运行环境变化,可能包含:
1 | 当前脚本所在目录 |
实际输出通常是多个文件系统路径,不一定与上面的描述完全相同。
sys.path 是一个列表,列出了 Python 在查找模块时使用的搜索路径。具体搜索行为还会受到导入机制、包结构和模块加载器等因素影响。
初学阶段可以先记住:当 Python 找不到要导入的模块时,首先要检查模块文件是否放在合适的位置,以及当前程序的搜索路径是否包含该位置。
使用包组织多个模块
当项目只有一两个文件时,直接使用模块通常就够了。
随着项目增大,我们可以把多个相关模块组织成一个包(Package)。
例如,创建以下目录结构:
1 | 06_module_path/ |
这里:
main.py是调用程序。tools/是存放相关功能模块的目录。__init__.py可以让tools以传统的 Python 包形式组织起来。math_demo.py是包中的一个模块。
创建 tools/math_demo.py:
1 | def add(a, b): |
tools/__init__.py 可以先留空。
然后在 main.py 中写入:
1 | from tools.math_demo import add |
在 06_module_path 目录下运行:
1 | python main.py |
输出:
1 | 30 |
Python 找到了 tools 包中的 math_demo 模块,并导入其中的 add() 函数。
这里的 tools.math_demo 可以理解为从包到模块的层级路径。
需要注意,现代 Python 也支持不包含 __init__.py 的命名空间包,因此 __init__.py 并非所有包结构都必须具备。不过,对于初学者和很多常规项目来说,保留这个文件有助于清楚地表达包的结构。
实战:把服务器磁盘检查功能拆分成模块
前面学习了 import、模块执行顺序、缓存和包结构。接下来,把这些知识用到一个简单的 Linux 运维场景中。
假设我们希望编写一个程序,检查 Linux 服务器的根文件系统使用率,并在使用率达到阈值时输出告警。
这里先使用 Python 标准库中的 shutil 获取磁盘空间信息,不依赖第三方模块。
创建项目目录
在终端中执行:
1 | mkdir -p 07_server_project/server_check |
最终的项目结构如下:
1 | 07_server_project/ |
三个文件分别负责:
__init__.py:标识并组织包,可以先留空。disk_check.py:实现磁盘检查功能。main.py:程序入口,负责调用检查功能。
编写磁盘检查模块
创建 server_check/disk_check.py:
1 | import shutil |
这段代码的核心是:
1 | total, used, free = shutil.disk_usage(path) |
shutil.disk_usage() 会返回指定路径所在文件系统的总容量、已使用容量和可用容量。
这里使用 / 作为默认路径,表示检查根文件系统。容量除以 1024 ** 3 后,以 GiB 为单位显示。
然后通过:
1 | usage = used / total * 100 |
计算已使用容量占总容量的百分比。
最后用 if 判断是否达到 80% 的告警阈值。这个阈值只是演示用的示例,实际生产环境应根据业务和文件系统的使用情况设置。
编写程序入口
创建 server_check/main.py:
1 | from .disk_check import check_disk |
这里使用了:
1 | from .disk_check import check_disk |
开头的点号表示从当前包中导入 disk_check 模块里的 check_disk() 函数。
由于使用了相对导入,我们通过包的模块路径启动程序,而不是直接把 main.py 当成普通脚本运行。
运行程序
确认当前目录是 07_server_project,执行:
1 | python -m server_check.main |
如果系统使用 python3,则执行:
1 | python3 -m server_check.main |
输出示例:
1 | 检查路径:/ |
注意,上面的容量和使用率只是示例,实际输出取决于运行程序的 Linux 服务器。
这里的 -m 表示让 Python 按模块的方式查找并执行 server_check.main。因为我们从项目目录启动,Python 可以识别 server_check 包及其内部模块,相对导入也就能够正常工作。
整个项目的调用关系可以概括为:
flowchart TD
A["执行 python -m server_check.main"] --> B["加载 server_check.main"]
B --> C["导入 disk_check 模块"]
C --> D["定义 check_disk() 函数"]
D --> E["执行 main()"]
E --> F["调用 check_disk('/')"]
F --> G["获取文件系统容量"]
G --> H["计算磁盘使用率"]
H --> I{"使用率是否达到 80%?"}
I -->|是| J["输出告警"]
I -->|否| K["输出正常状态"]这个例子虽然简单,但已经体现了实际项目中常见的结构:功能模块负责具体工作,入口模块负责组织调用。
后续如果要继续增加内存检查、CPU 检查和服务状态检查,只需分别创建相应模块,再由 main.py 组织执行即可。
常见问题
为什么提示 No module named xxx?
这通常表示 Python 没有找到要导入的模块。可以检查:
- 模块文件是否存在,文件名是否拼写正确。
- 程序启动时所在目录是否符合预期。
- 模块所在目录是否在 Python 的搜索路径中。
- 如果是第三方库,当前 Python 环境中是否已经安装。
如果机器上安装了多个 Python 版本,还要确认安装依赖和运行程序时使用的是同一个解释器。
为什么模块导入时会输出内容?
因为模块首次加载时,顶层代码会执行。
如果不希望某段逻辑在导入时自动运行,可以把它放进函数中,并在适当的位置调用;对于程序入口逻辑,也可以使用 if __name__ == "__main__":。
为什么重复导入没有重复执行?
因为 Python 通常会使用 sys.modules 中缓存的模块对象。同一个解释器进程中,后续导入一般会复用已有模块,而不是重新执行整个模块。
模块、包和第三方库有什么区别?
- 模块:通常指一个 Python 文件。
- 包:用于组织多个模块的结构。
- 第三方库:由 Python 标准库之外的项目提供的软件组件,可能包含一个或多个包和模块。
这些概念并不完全处于同一分类层级,但理解它们的区别,有助于阅读项目目录和安装说明。
写在最后
到这里,我们已经掌握了 Python 模块的基本使用方法:
- 使用
import导入模块。 - 使用
from ... import ...导入指定名称。 - 使用
as设置别名。 - 区分内置模块、标准库模块和第三方模块。
- 理解模块首次导入时的代码执行顺序。
- 认识
sys.modules以及模块缓存机制。 - 使用
sys.path理解模块搜索路径。 - 使用包组织代码,并在实际项目中拆分功能模块。
学习模块,不只是记住几种导入语法,更重要的是开始改变编写程序的方式:把不同职责的代码拆分开,让程序更容易阅读、复用和维护。
下一篇,我们继续学习一个实用的内置函数:dir()。它可以帮助我们查看对象或模块中有哪些名称,方便进一步探索 Python 模块提供的功能。
