模块与导入
当程序只有几十行时,把代码写在一个文件里通常没有问题。随着功能增加,函数、类和数据会越来越多,一个文件就会变得难以阅读和维护。如何在不改变功能的前提下拆分代码,并让不同文件中的功能可以复用?
这时可以按照功能拆分代码:学生数据放在一个文件中,菜单功能放在另一个文件中,程序入口只负责组织调用。这些独立的 Python 文件,就是模块。本节先完成一个多文件程序,再比较导入方式、导入机制和模块搜索路径。
模块定义
在 Python 中,一个 .py 文件就是一个模块。模块中可以定义:
- 变量
- 函数
- 类
- 可以直接执行的语句
例如,文件 student_tools.py 中定义了学生列表和两个函数:
print("student_tools 模块开始加载")
student_list = []
def add_student(name):
student_list.append(name)
print(f"已添加学生:{name}")
def show_students():
print("学生列表:")
for index, name in enumerate(student_list, start=1):
print(f"{index}. {name}")另一个文件可以导入并使用这个模块。
案例:学生工具模块拆分
需求:把学生数据和操作拆分到 student_tools.py,由 main.py 作为程序入口完成调用。
功能分析:先定义可复用的学生操作,再在入口文件中导入模块并运行,观察两个文件如何分工。
新建一个目录,并创建下面两个文件:
01-student-tools/
├── main.py
└── student_tools.pystudent_tools.py 保存学生数据和学生操作,代码使用上一节的内容。
在 main.py 中导入模块:
import student_tools
student_tools.add_student("张三")
student_tools.add_student("李四")
student_tools.show_students()进入示例目录后运行:
python3 main.py运行结果:
student_tools 模块开始加载
已添加学生:张三
已添加学生:李四
学生列表:
1. 张三
2. 李四两个文件承担的职责不同:
| 文件 | 职责 |
|---|---|
main.py | 作为程序入口,组织功能的调用顺序 |
student_tools.py | 保存数据并提供可以复用的功能 |
这种拆分不会减少代码量,但会让每个文件的职责更清楚。
导入方式
模块拆分后,如何选择导入写法?下面比较模块导入、指定名称导入、别名导入和星号导入的差异。
模块导入
import student_tools
student_tools.add_student("张三")调用时需要写出模块名。代码稍长,但能清楚看出函数来自哪个模块,基础阶段优先使用这种方式。
指定名称导入
from student_tools import add_student, show_students
add_student("张三")
show_students()这种写法调用更短,适合只使用模块中的少量内容。如果当前文件中也有同名函数,就要注意名称冲突。
别名导入
模块名较长,或者名称发生冲突时,可以使用 as 设置别名:
import student_tools as tools
tools.add_student("张三")也可以给导入的函数设置别名:
from student_tools import add_student as create_student
create_student("张三")别名应该简短且容易理解,不要为了少写几个字符使用含义不明的缩写。
星号导入风险
from student_tools import *这种写法会把模块中可导入的名称一次性放入当前文件。代码很难看出名称来自哪里,也更容易覆盖已有名称,因此不建议在课程项目中使用。
模块导入机制
执行 import student_tools 时,Python 不只是找到这个文件,还会执行模块中的顶层代码。函数体只有在调用函数时才会执行,但模块最外层的语句会在导入过程中执行。理解这个过程,才能解释导入时的输出和重复导入行为。
因此,示例中的下面一行会在导入时输出:
print("student_tools 模块开始加载")在同一次程序运行中,再次导入已经加载的模块,Python 通常会直接复用已加载的模块,不会重新执行模块代码:
import student_tools
import student_tools运行时仍然只会看到一次“模块开始加载”。
这一点说明:模块顶层代码会影响导入它的文件。可复用模块通常以变量、函数和类的定义为主,不要随意在顶层放置业务流程。
模块搜索路径
导入模块时,Python 如何找到对应文件?Python 会根据模块搜索路径查找,搜索路径记录在 sys.path 中:
import sys
for path in sys.path:
print(path)搜索路径通常包含程序入口所在目录、Python 标准库目录和已经安装的第三方库目录。具体第一项会受到启动方式影响,因此不能简单理解为永远等于“当前工作目录”。
前面的示例能够直接导入 student_tools,是因为它和入口文件 main.py 位于同一个目录,这个目录在当前启动方式下可以被 Python 搜索到。
模块命名冲突
自定义模块为什么不能随意使用标准库或常用第三方库的名称?例如:
random.py
json.py
logging.py假设当前目录中存在自己创建的 random.py,执行下面的代码时,Python 可能优先导入这个文件,而不是标准库中的 random:
import random如果导入结果与预期不符,可以查看模块实际来自哪里:
import random
print(random.__file__)发现名称冲突后,应修改自定义文件名,并清理同目录下对应的缓存文件,再重新运行程序。
小结
- 一个
.py文件就是一个模块,可以保存变量、函数和类。 import 模块名能清楚标明内容来源,适合作为默认导入方式。from 模块名 import 名称可以缩短调用代码,但需要注意名称冲突。- 模块第一次被导入时会执行顶层代码,同一次运行中重复导入通常会复用已加载的模块。
- Python 根据模块搜索路径查找模块,自定义文件名应避免与现有模块冲突。
练习
- 创建
calculator.py,在其中定义加法和减法函数,再从main.py中导入并调用。 - 分别使用
import calculator和from calculator import add完成调用,比较两种写法。 - 在
calculator.py顶层输出一行提示,连续导入两次,观察提示出现几次。 - 输出
calculator.__file__,确认 Python 实际导入的是哪个文件。