包与包内导入
模块可以把代码从一个大文件拆成多个小文件。当模块继续增加时,如果所有 .py 文件仍然堆在同一个目录中,查找和管理也会变得困难:如何继续组织这些模块,并让导入关系保持清晰?
包用于解决这个问题:把一组相关模块放进同一个目录,让目录也成为代码结构的一部分。本章通过一个学生系统示例,完成包结构、包内导入和入口边界的整理。
包定义
在本课程的项目中,包是一个包含 __init__.py 的目录:
student_system/
├── __init__.py
├── menu.py
└── student_tools.py其中:
student_system是包名。menu.py和student_tools.py是包中的模块。__init__.py用来标识和初始化这个包,也可以整理包对外提供的名称。
Python 3.14 还支持没有 __init__.py 的命名空间包,但它适合更特殊的项目组织方式。基础课程中的普通项目统一保留 __init__.py,结构更明确,也更容易理解和排查问题。
案例:学生系统包结构
需求: 将菜单显示、学生数据操作拆分到 student_system 包中,由 main.py 作为程序入口。
功能分析: menu.py 负责菜单显示,student_tools.py 负责学生数据操作,__init__.py 负责包初始化和后续接口整理。
创建下面的项目结构:
01-student-system/
├── main.py
└── student_system/
├── __init__.py
├── menu.py
└── student_tools.pymenu.py 负责显示菜单:
def show_menu():
print("1. 添加学生")
print("2. 显示学生")
print("0. 退出系统")student_tools.py 负责保存数据和操作学生:
student_list = []
def add_student(name):
student_list.append(name)
print(f"已添加学生:{name}")
def show_students():
if not student_list:
print("暂无学生")
return
print("学生列表:")
for index, name in enumerate(student_list, start=1):
print(f"{index}. {name}")__init__.py 可以先保持为空。接下来从入口文件中导入包内模块。
包内导入方式
包建立后,入口文件如何访问其中的模块和函数?下面比较三种常见导入方式及其适用边界。
完整路径导入
import student_system.menu
student_system.menu.show_menu()这种写法完整展示了“包 → 模块 → 函数”的所属关系,但每次调用都要写出较长的名称。
包内模块导入
from student_system import menu
menu.show_menu()这里导入的是 student_system 包中的 menu 模块。调用时保留模块名,来源仍然比较清楚。
指定内容导入
from student_system.student_tools import add_student, show_students
add_student("张三")
show_students()这种写法直接导入函数,适合入口文件只需要少量明确功能的情况。
只写 import student_system,并不代表包中的所有子模块都会自动导入。能够通过包名直接使用哪些内容,取决于包的初始化过程和 __init__.py 中公开了什么。
包接口整理
如果入口文件不需要了解每个功能所在的模块,可以在 __init__.py 中统一导入,整理包的对外接口:
from .menu import show_menu
from .student_tools import add_student, show_students入口文件就可以写成:
from student_system import add_student, show_menu, show_students
show_menu()
add_student("张三")
add_student("李四")
show_students()进入示例目录后运行:
python3 main.py运行结果:
1. 添加学生
2. 显示学生
0. 退出系统
已添加学生:张三
已添加学生:李四
学生列表:
1. 张三
2. 李四__init__.py 不只是一个标记文件,它本身也是 Python 文件。第一次导入包时,其中的顶层代码会被执行。因此,这个文件适合完成简单的包初始化和接口整理,不适合堆放大量业务代码。
包内相对导入
包内部如何引用同包模块,同时让包外入口保持清晰?相对导入用点表示当前包或上一级包,适合包内部的模块关系。
__init__.py 中的导入语句以点开头:
from .menu import show_menu
from .student_tools import add_student, show_students这里的点表示包关系:
| 写法 | 含义 |
|---|---|
. | 当前包 |
.. | 上一级包 |
点不是普通文件路径中的当前目录和上级目录,而是由 Python 根据当前模块所属的包来解释。
相对导入通常用于包内部。位于包外的入口文件一般使用绝对导入,直接从顶层包名开始:
from student_system import show_menu这样能够明确程序从哪个包获取功能。
导入问题排查
导入失败时,先区分项目路径、启动方式和模块命名三类问题,再针对性检查。
包或模块未找到
ModuleNotFoundError: No module named 'student_system'先检查三件事:
- 包名、模块名和代码中的拼写是否完全一致。
main.py是否位于student_system的外层目录。- 是否从项目入口启动程序,而不是随意切换到包内部运行某个文件。
相对导入缺少父包
ImportError: attempted relative import with no known parent package如果一个包内模块使用了相对导入,却被当作普通脚本直接运行,Python 就不知道它属于哪个包。
例如,不要进入 student_system 后直接运行内部模块:
python3 menu.py当前案例应回到项目外层,运行入口文件:
python3 main.py在需要直接启动包内模块的项目中,也可以回到包的外层目录,使用 python3 -m 包名.模块名 按模块方式运行。此时 Python 才能正确建立包关系。
模块名称冲突
如果项目中存在 json.py、random.py 等文件,可能会覆盖原本想导入的标准库模块。可以检查模块的 __file__ 属性,确认实际导入位置,再修改冲突的文件名。
小结
- 包用于组织一组相关模块,使大型项目的目录结构更清楚。
- 本课程中的普通包保留
__init__.py,用于标识、初始化和整理包接口。 - 可以导入完整模块、包内模块或模块中的指定内容。
.和..表示包的层级关系,不是普通文件系统路径。- 入口文件通常使用绝对导入,包内部可以根据结构使用相对导入。
- 遇到导入错误时,先检查项目结构、启动位置、名称拼写和实际导入路径。
练习
- 创建
shop包,在其中编写product.py和order.py两个模块。 - 在
shop/__init__.py中统一导出两个模块中的主要函数,再从main.py调用。 - 分别尝试导入整个模块和导入指定函数,比较调用方式。
- 直接运行一个包含相对导入的包内模块,观察报错;再从外层入口正确运行。