Skip to content

包与包内导入 ​

模块可以把代码从一个大文件拆成多个小文件。当模块继续增加时,如果所有 .py 文件仍然堆在同一个目录中,查找和管理也会变得困难:如何继续组织这些模块,并让导入关系保持清晰?

包用于解决这个问题:把一组相关模块放进同一个目录,让目录也成为代码结构的一部分。本章通过一个学生系统示例,完成包结构、包内导入和入口边界的整理。

包定义 ​

在本课程的项目中,包是一个包含 __init__.py 的目录:

text
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 负责包初始化和后续接口整理。

创建下面的项目结构:

text
01-student-system/
├── main.py
└── student_system/
    ├── __init__.py
    ├── menu.py
    └── student_tools.py

menu.py 负责显示菜单:

python
def show_menu():
    print("1. 添加学生")
    print("2. 显示学生")
    print("0. 退出系统")

student_tools.py 负责保存数据和操作学生:

python
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 可以先保持为空。接下来从入口文件中导入包内模块。

包内导入方式 ​

包建立后,入口文件如何访问其中的模块和函数?下面比较三种常见导入方式及其适用边界。

完整路径导入 ​

python
import student_system.menu

student_system.menu.show_menu()

这种写法完整展示了“包 → 模块 → 函数”的所属关系,但每次调用都要写出较长的名称。

包内模块导入 ​

python
from student_system import menu

menu.show_menu()

这里导入的是 student_system 包中的 menu 模块。调用时保留模块名,来源仍然比较清楚。

指定内容导入 ​

python
from student_system.student_tools import add_student, show_students

add_student("张三")
show_students()

这种写法直接导入函数,适合入口文件只需要少量明确功能的情况。

只写 import student_system,并不代表包中的所有子模块都会自动导入。能够通过包名直接使用哪些内容,取决于包的初始化过程和 __init__.py 中公开了什么。

包与包内导入路径

包接口整理 ​

如果入口文件不需要了解每个功能所在的模块,可以在 __init__.py 中统一导入,整理包的对外接口:

python
from .menu import show_menu
from .student_tools import add_student, show_students

入口文件就可以写成:

python
from student_system import add_student, show_menu, show_students

show_menu()
add_student("张三")
add_student("李四")
show_students()

进入示例目录后运行:

shell
python3 main.py

运行结果:

text
1. 添加学生
2. 显示学生
0. 退出系统
已添加学生:张三
已添加学生:李四
学生列表:
1. 张三
2. 李四

__init__.py 不只是一个标记文件,它本身也是 Python 文件。第一次导入包时,其中的顶层代码会被执行。因此,这个文件适合完成简单的包初始化和接口整理,不适合堆放大量业务代码。

init.py 对外接口与入口边界

包内相对导入 ​

包内部如何引用同包模块,同时让包外入口保持清晰?相对导入用点表示当前包或上一级包,适合包内部的模块关系。

__init__.py 中的导入语句以点开头:

python
from .menu import show_menu
from .student_tools import add_student, show_students

这里的点表示包关系:

写法含义
.当前包
..上一级包

点不是普通文件路径中的当前目录和上级目录,而是由 Python 根据当前模块所属的包来解释。

相对导入通常用于包内部。位于包外的入口文件一般使用绝对导入,直接从顶层包名开始:

python
from student_system import show_menu

这样能够明确程序从哪个包获取功能。

导入问题排查 ​

导入失败时,先区分项目路径、启动方式和模块命名三类问题,再针对性检查。

包或模块未找到 ​

text
ModuleNotFoundError: No module named 'student_system'

先检查三件事:

  1. 包名、模块名和代码中的拼写是否完全一致。
  2. main.py 是否位于 student_system 的外层目录。
  3. 是否从项目入口启动程序,而不是随意切换到包内部运行某个文件。

相对导入缺少父包 ​

text
ImportError: attempted relative import with no known parent package

如果一个包内模块使用了相对导入,却被当作普通脚本直接运行,Python 就不知道它属于哪个包。

例如,不要进入 student_system 后直接运行内部模块:

shell
python3 menu.py

当前案例应回到项目外层,运行入口文件:

shell
python3 main.py

在需要直接启动包内模块的项目中,也可以回到包的外层目录,使用 python3 -m 包名.模块名 按模块方式运行。此时 Python 才能正确建立包关系。

模块名称冲突 ​

如果项目中存在 json.py、random.py 等文件,可能会覆盖原本想导入的标准库模块。可以检查模块的 __file__ 属性,确认实际导入位置,再修改冲突的文件名。

小结 ​

  • 包用于组织一组相关模块,使大型项目的目录结构更清楚。
  • 本课程中的普通包保留 __init__.py,用于标识、初始化和整理包接口。
  • 可以导入完整模块、包内模块或模块中的指定内容。
  • . 和 .. 表示包的层级关系,不是普通文件系统路径。
  • 入口文件通常使用绝对导入,包内部可以根据结构使用相对导入。
  • 遇到导入错误时,先检查项目结构、启动位置、名称拼写和实际导入路径。

练习 ​

  1. 创建 shop 包,在其中编写 product.py 和 order.py 两个模块。
  2. 在 shop/__init__.py 中统一导出两个模块中的主要函数,再从 main.py 调用。
  3. 分别尝试导入整个模块和导入指定函数,比较调用方式。
  4. 直接运行一个包含相对导入的包内模块,观察报错;再从外层入口正确运行。