5.7 包与项目组织
任务管理器从单文件发展到多模块后,文件数量继续增长,把所有 .py 文件平铺在一个目录里会再次变得混乱。数据模型、界面显示、工具函数混在一起,找代码都要费劲。Python 的包机制就是为了解决这个问题:把相关的模块放进一个目录,目录名作为包名,形成层级结构。这样任务管理器可以组织成包含 models/、ui/、utils/ 子包的项目,结构清晰、职责分明。本节讲解包的概念、__init__.py 的作用、绝对导入与相对导入,以及包的合理拆分原则。
5.7.1 包的概念
包是一种特殊的模块,它是一个包含 __init__.py 文件的目录。目录名就是包名,目录内的 .py 文件是该包的子模块。包可以嵌套,形成多级层级结构,类似文件系统的目录树。包让大量模块可以按功能分组组织,避免名称冲突,提升代码的可维护性。
Python 3.3 之后,没有 __init__.py 的目录也能作为包使用,称为命名空间包。但常规项目仍建议使用带 __init__.py 的普通包,行为更明确、兼容性更好。本节讨论的都是普通包。
任务管理器组织成如下包结构。
task_project/
__init__.py
main.py
models/
__init__.py
task.py
ui/
__init__.py
display.py
input.py
utils/
__init__.py
date_util.py
validator.pytask_project 是顶层包,models、ui、utils 是子包,每个子包内的 .py 文件是子模块。每个包含 __init__.py 的目录都是一个包。
5.7.2 __init__.py 的作用
__init__.py 文件标志一个目录是 Python 包。它的作用有几个方面:标识包的存在、在包被导入时执行初始化代码、控制 from 包名 import * 的行为、简化包的对外接口。
__init__.py 可以是空文件,只起到标识作用。也可以写入代码,这些代码在包被首次导入时执行。
# task_project/models/__init__.py
from .task import Task
__all__ = ["Task"]# task_project/models/task.py
class Task:
def __init__(self, title, priority=3):
self.title = title
self.priority = priority当外部代码写 from task_project.models import Task 时,Python 先执行 models/__init__.py,其中 from .task import Task 把 Task 类导入到 models 包的命名空间。这样外部就能直接从 models 包导入 Task,而不必写更长的 from task_project.models.task import Task。__init__.py 起到了简化对外接口的作用。
5.7.3 包的层级结构与导入
包内的模块通过点号路径访问。导入子模块时,写出完整的包路径,如 import task_project.models.task。导入后用完整路径访问模块内容。
import task_project.models.task
t = task_project.models.task.Task("写报告")
print(t.title)完整路径写起来较长,通常用 from...import 直接导入需要的名称。
from task_project.models.task import Task
t = Task("写报告")from task_project.models.task import Task 直接把 Task 类导入当前命名空间,使用时无需前缀。包的层级结构让模块名不会冲突,即使两个子包都有 task.py,通过包路径也能区分。
5.7.4 绝对导入
绝对导入使用从项目根目录开始的完整包路径来导入模块。这是 Python 推荐的导入方式,路径清晰明确,能直观看出模块的位置。
# task_project/ui/display.py
from task_project.models.task import Task
from task_project.utils.date_util import format_date
def show_task(task):
print(f"[{task.priority}] {task.title} - {format_date()}")from task_project.models.task import Task 是绝对导入,从顶层包 task_project 开始写出完整路径。绝对导入的好处是路径明确,无论当前模块在哪个位置,导入语句都一样。缺点是包层级很深时路径较长,且如果包被重命名,所有绝对导入都要修改。
5.7.5 相对导入
相对导入使用 . 和 .. 前缀,相对于当前模块所在的包来导入。一个点 . 表示当前包,两个点 .. 表示父包,三个点 ... 表示祖父包。
# task_project/ui/display.py
from . import input # 导入同包下的 input 模块
from ..models.task import Task # 导入上级包 models 下的 task 模块
from ..utils.date_util import format_date # 导入上级包 utils 下的 date_util 模块from . import input 中 . 表示当前包 ui,导入同级的 input 模块。from ..models.task import Task 中 .. 表示父包 task_project,从中导入 models.task。相对导入让包内部的模块互相关联时不必写完整路径,包被重命名时导入语句也无需修改。
5.7.6 相对导入的限制
相对导入只能在包内部使用,不能用于顶层脚本(直接运行的 .py 文件)。顶层脚本的 __package__ 为 None,相对导入会报错。这是相对导入最常见的使用障碍。
# task_project/main.py 直接运行时
from .models.task import Task # 报错:attempted relative import with no known parent package如果直接用 python main.py 运行 main.py,其中的相对导入会失败。解决方法有两种:用 python -m task_project.main 以模块方式运行,让 Python 知道它属于 task_project 包;或者把可执行入口放在包外部,用绝对导入。
5.7.7 绝对导入与相对导入的选择
绝对导入和相对导入各有优劣。绝对导入路径清晰,IDE 跳转友好,是 Python 官方推荐的方式。相对导入在包内部使用时更简洁,包重命名时无需修改,但只限包内使用,且容易在层级深时点号过多导致难以理解。
一般建议优先使用绝对导入,特别是在包层级不深的项目中。包内部联系紧密的模块之间,如果包可能被重命名或移动,可以考虑相对导入。无论选择哪种,同一个项目内应保持一致,避免混用造成困惑。
5.7.8 __init__.py 控制包的接口
__init__.py 不仅能标识包,还能用来控制包的对外接口。通过在 __init__.py 中导入子模块的名称,可以让使用者直接从包名导入,而不必关心内部文件结构。
# task_project/__init__.py
from .models.task import Task
from .ui.display import show_task
__all__ = ["Task", "show_task"]有了这个 __init__.py,外部代码可以这样使用。
from task_project import Task, show_task
t = Task("写报告")
show_task(t)外部使用者只需 from task_project import Task,不必知道 Task 类定义在 models/task.py 中。包的内部结构可以自由调整,只要 __init__.py 的导出接口不变,外部代码就不受影响。这是封装实现细节的有效手段。
5.7.9 包的合理拆分
把项目拆分成包和子包时,应遵循高内聚低耦合的原则。高内聚指同一个包内的模块功能相关、互相协作密切。低耦合指不同包之间的依赖尽量少、接口尽量简单。
任务管理器按职责拆分:models 包放数据模型,ui 包放界面显示,utils 包放通用工具。这种拆分让每个包聚焦一个职责,包内模块互相依赖紧密,包间依赖较少且方向清晰。
task_project/
models/ # 数据模型层,定义 Task 等数据结构
__init__.py
task.py
ui/ # 界面层,负责显示和输入
__init__.py
display.py
input.py
utils/ # 工具层,提供通用辅助函数
__init__.py
date_util.py
validator.py
__init__.py
main.py # 程序入口依赖方向应该是上层依赖下层:ui 依赖 models 和 utils,models 依赖 utils,utils 不依赖其他包。这种单向依赖避免了循环,让结构清晰。如果发现 utils 需要用到 models 的内容,说明职责划分有问题,应重新调整。
拆分粒度也要把握好。包太小(每个包只有一个模块)会增加目录层级和导入路径的复杂度,包太大(所有东西塞在一个包里)又失去了拆分的意义。通常按业务领域或功能层次来划分,每个包包含若干个紧密相关的模块。
包设计的核心原则
高内聚低耦合是包设计的核心原则。让相关的代码聚在一起,让无关的代码分开。依赖方向保持单向,从高层到低层。包的对外接口通过 __init__.py 控制,隐藏内部实现细节。这些原则在项目规模增长时能显著降低维护成本。
练习题
练习 1 假设有以下包结构,请写出在 main.py 中导入 Task 类的三种方式(绝对导入、相对导入、通过包的 __init__.py)。
task_project/
__init__.py
main.py
models/
__init__.py
task.py # 定义了 Task 类参考答案
# 方式一:绝对导入
from task_project.models.task import Task
# 方式二:相对导入(main.py 在 task_project 包内)
from .models.task import Task
# 注意:相对导入要求以模块方式运行 python -m task_project.main
# 方式三:通过 __init__.py 简化
# 先在 task_project/models/__init__.py 中写入:
# from .task import Task
# 然后在 main.py 中:
from task_project.models import Task方式一路径最明确,推荐使用。方式二适合包内部,但直接运行 main.py 会报错,需用 python -m 运行。方式三需要在 models/__init__.py 中先导出 Task,外部导入更简洁,是封装接口的常用做法。
练习 2 以下代码在 task_project/ui/display.py 中使用了相对导入,但直接运行该文件时报错 attempted relative import with no known parent package。请说明原因并给出两种解决方法。
# task_project/ui/display.py
from ..models.task import Task
def show(task):
print(task.title)参考答案
原因:直接运行 python display.py 时,Python 把 display.py 当作顶层脚本,其 __package__ 为 None,相对导入无法确定父包,因此报错。相对导入只能用于包内部的模块,不能用于直接运行的脚本。
解决方法一:用模块方式运行,从项目根目录执行 python -m task_project.ui.display。这样 Python 知道 display 属于 task_project.ui 包,相对导入能正常工作。
解决方法二:改用绝对导入,相对导入改为 from task_project.models.task import Task。绝对导入不依赖 __package__,直接运行也能工作(前提是 task_project 在搜索路径中)。
实际项目中,通常不在子模块中放可执行代码,而是用一个独立的入口文件(如项目根目录的 main.py)来调用包内的功能,避免直接运行包内模块。
练习 3 为 task_project/utils 包编写 __init__.py,使得外部可以直接 from task_project.utils import format_date, validate_title 导入这两个函数(假设它们分别定义在 date_util.py 和 validator.py 中)。
参考答案
# task_project/utils/date_util.py
def format_date():
from datetime import date
return date.today().isoformat()# task_project/utils/validator.py
def validate_title(title):
return bool(title and title.strip())# task_project/utils/__init__.py
from .date_util import format_date
from .validator import validate_title
__all__ = ["format_date", "validate_title"]外部使用如下。
from task_project.utils import format_date, validate_title
print(format_date()) # 2026-08-01(当前日期)
print(validate_title("写报告")) # True
print(validate_title("")) # False__init__.py 中用相对导入把子模块的函数引入包命名空间,外部就能直接从 utils 包导入。__all__ 进一步明确了公开接口。这样 utils 内部文件结构的调整不影响外部导入语句。
练习 4 一个任务管理器项目有以下依赖关系:ui 模块需要用 models 的 Task 类和 utils 的格式化函数,models 需要用 utils 的校验函数,utils 不依赖其他模块。请画出依赖方向,并说明这种设计是否符合高内聚低耦合原则。
参考答案
依赖方向如下。
ui --> models --> utils
\------------------^ui 依赖 models 和 utils,models 依赖 utils,utils 不依赖任何其他包。箭头方向从高层指向低层,形成单向依赖,没有循环。
这种设计符合高内聚低耦合原则。高内聚体现在每个包职责单一:models 专注数据模型,ui 专注界面展示,utils 专注通用工具。低耦合体现在包间依赖方向单一,utils 作为最底层不依赖任何包,可以被任意上层使用;models 只依赖 utils;ui 依赖前两者。如果 utils 试图依赖 models,就形成了循环依赖,违背了原则。
对应的包结构如下。
task_project/
models/
task.py # 依赖 utils.validator
ui/
display.py # 依赖 models.Task 和 utils.format_date
utils/
validator.py
date_util.py这种分层结构在中小型项目中很常见,扩展时只需在对应层级添加模块,不会打乱整体依赖关系。
常见错误
错误 1 · ImportError: attempted relative import with no known parent package
原因:直接用 python task_project/ui/display.py 运行包内模块,此时该模块的 __package__ 为 None,相对导入无法确定父包。
解决:改用 python -m task_project.ui.display 从项目根目录以模块方式运行,让 Python 知道该模块所属的包;或把相对导入改为绝对导入。
错误 2 · ModuleNotFoundError: No module named 'task_project'
原因:包目录不在 sys.path 搜索路径中。常见于执行目录不是项目根目录,或包没有正确安装到 Python 环境中。
解决:从项目根目录运行程序,让顶层包在搜索路径中可见。或设置 PYTHONPATH 指向项目根目录。把包安装到 site-packages 中是更规范的做法。
错误 3 · 包目录缺少 __init__.py 导致导入异常
现象:包内模块的相对导入、from 包名 import 名称 等行为不符合预期,或包被识别为命名空间包而非普通包。
原因:目录下没有 __init__.py 文件。Python 3.3 之后虽然支持命名空间包,但行为与普通包有差异,__init__.py 中的初始化代码和接口导出也不会执行。
解决:在每个包目录下创建 __init__.py,可以是空文件,也可以写入初始化代码和 __all__、子模块导出等。
错误 4 · 相对导入点号层级错误
现象:ImportError: attempted relative import beyond top-level package。
原因:相对导入使用的 .. 层数超过了顶层包。例如在 task_project/ui/display.py 中写 ...models.task,三个点表示祖父包,但 task_project 已是顶层包,再往上就超出了范围。
解决:数清当前模块所在的包层级,相对导入的点号不超过顶层包。层级过深时改用绝对导入更清晰。