Skip to content

模块与导入 ​

当程序只有几十行时,把代码写在一个文件里通常没有问题。随着功能增加,函数、类和数据会越来越多,一个文件就会变得难以阅读和维护。如何在不改变功能的前提下拆分代码,并让不同文件中的功能可以复用?

这时可以按照功能拆分代码:学生数据放在一个文件中,菜单功能放在另一个文件中,程序入口只负责组织调用。这些独立的 Python 文件,就是模块。本节先完成一个多文件程序,再比较导入方式、导入机制和模块搜索路径。

模块定义 ​

在 Python 中,一个 .py 文件就是一个模块。模块中可以定义:

  • 变量
  • 函数
  • 类
  • 可以直接执行的语句

例如,文件 student_tools.py 中定义了学生列表和两个函数:

python
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 作为程序入口完成调用。

功能分析:先定义可复用的学生操作,再在入口文件中导入模块并运行,观察两个文件如何分工。

新建一个目录,并创建下面两个文件:

text
01-student-tools/
├── main.py
└── student_tools.py

student_tools.py 保存学生数据和学生操作,代码使用上一节的内容。

在 main.py 中导入模块:

python
import student_tools

student_tools.add_student("张三")
student_tools.add_student("李四")
student_tools.show_students()

进入示例目录后运行:

shell
python3 main.py

运行结果:

text
student_tools 模块开始加载
已添加学生:张三
已添加学生:李四
学生列表:
1. 张三
2. 李四

两个文件承担的职责不同:

文件职责
main.py作为程序入口,组织功能的调用顺序
student_tools.py保存数据并提供可以复用的功能

这种拆分不会减少代码量,但会让每个文件的职责更清楚。

多文件程序的模块职责

导入方式 ​

模块拆分后,如何选择导入写法?下面比较模块导入、指定名称导入、别名导入和星号导入的差异。

模块导入 ​

python
import student_tools

student_tools.add_student("张三")

调用时需要写出模块名。代码稍长,但能清楚看出函数来自哪个模块,基础阶段优先使用这种方式。

指定名称导入 ​

python
from student_tools import add_student, show_students

add_student("张三")
show_students()

这种写法调用更短,适合只使用模块中的少量内容。如果当前文件中也有同名函数,就要注意名称冲突。

别名导入 ​

模块名较长,或者名称发生冲突时,可以使用 as 设置别名:

python
import student_tools as tools

tools.add_student("张三")

也可以给导入的函数设置别名:

python
from student_tools import add_student as create_student

create_student("张三")

别名应该简短且容易理解,不要为了少写几个字符使用含义不明的缩写。

常见导入方式对比

星号导入风险 ​

python
from student_tools import *

这种写法会把模块中可导入的名称一次性放入当前文件。代码很难看出名称来自哪里,也更容易覆盖已有名称,因此不建议在课程项目中使用。

模块导入机制 ​

执行 import student_tools 时,Python 不只是找到这个文件,还会执行模块中的顶层代码。函数体只有在调用函数时才会执行,但模块最外层的语句会在导入过程中执行。理解这个过程,才能解释导入时的输出和重复导入行为。

因此,示例中的下面一行会在导入时输出:

python
print("student_tools 模块开始加载")

在同一次程序运行中,再次导入已经加载的模块,Python 通常会直接复用已加载的模块,不会重新执行模块代码:

python
import student_tools
import student_tools

运行时仍然只会看到一次“模块开始加载”。

模块导入的生命周期与搜索路径

这一点说明:模块顶层代码会影响导入它的文件。可复用模块通常以变量、函数和类的定义为主,不要随意在顶层放置业务流程。

模块搜索路径 ​

导入模块时,Python 如何找到对应文件?Python 会根据模块搜索路径查找,搜索路径记录在 sys.path 中:

python
import sys

for path in sys.path:
    print(path)

搜索路径通常包含程序入口所在目录、Python 标准库目录和已经安装的第三方库目录。具体第一项会受到启动方式影响,因此不能简单理解为永远等于“当前工作目录”。

前面的示例能够直接导入 student_tools,是因为它和入口文件 main.py 位于同一个目录,这个目录在当前启动方式下可以被 Python 搜索到。

模块命名冲突 ​

自定义模块为什么不能随意使用标准库或常用第三方库的名称?例如:

text
random.py
json.py
logging.py

假设当前目录中存在自己创建的 random.py,执行下面的代码时,Python 可能优先导入这个文件,而不是标准库中的 random:

python
import random

如果导入结果与预期不符,可以查看模块实际来自哪里:

python
import random

print(random.__file__)

发现名称冲突后,应修改自定义文件名,并清理同目录下对应的缓存文件,再重新运行程序。

小结 ​

  • 一个 .py 文件就是一个模块,可以保存变量、函数和类。
  • import 模块名 能清楚标明内容来源,适合作为默认导入方式。
  • from 模块名 import 名称 可以缩短调用代码,但需要注意名称冲突。
  • 模块第一次被导入时会执行顶层代码,同一次运行中重复导入通常会复用已加载的模块。
  • Python 根据模块搜索路径查找模块,自定义文件名应避免与现有模块冲突。

练习 ​

  1. 创建 calculator.py,在其中定义加法和减法函数,再从 main.py 中导入并调用。
  2. 分别使用 import calculator 和 from calculator import add 完成调用,比较两种写法。
  3. 在 calculator.py 顶层输出一行提示,连续导入两次,观察提示出现几次。
  4. 输出 calculator.__file__,确认 Python 实际导入的是哪个文件。