本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。
学习目标
学完本节后,你应当能够:
- 理解为什么不能把所有代码写在一个文件里。
- 区分函数、模块、包三个概念。
- 使用
import组织代码。 - 为后续项目搭建更清晰的目录结构。
前置知识
- 已完成
01-Python最小入门.md
1. 为什么需要拆文件
初学时,把所有代码写进一个 main.py 很常见。但只要你开始做下面这些事,单文件就会失控:
- 配置管理
- 模型调用
- 数据模型定义
- 工具函数复用
- 测试
月份 1 的项目必须从一开始就有模块边界。
2. 函数、模块、包分别是什么
函数
函数是最小的可复用逻辑单元。
def build_greeting(name: str) -> str:
return f"Hello, {name}"
模块
一个 .py 文件就是一个模块。
例如:
utils.py
里面可以放多个函数、类和常量。
包
一个包含多个模块的目录通常可组织成包。
例如:
app/
├── __init__.py
├── config.py
└── utils.py
app 就是一个包。
3. 最小模块拆分示例
假设你要写一个小聊天程序,可以先拆成:
project/
├── main.py
├── models.py
└── utils.py
models.py
class ChatMessage:
def __init__(self, role: str, content: str) -> None:
self.role = role
self.content = content
utils.py
def normalize_text(text: str) -> str:
return text.strip()
main.py
from models import ChatMessage
from utils import normalize_text
message = ChatMessage(role="user", content=" hello ")
print(normalize_text(message.content))
4. import 的基本规则
导入函数
from utils import normalize_text
导入模块
import utils
print(utils.normalize_text(" hi "))
导入类
from models import ChatMessage
原则:
- 优先清晰,不要追求花哨导入方式
- 不要用
from x import * - 模块命名尽量简洁且表达职责
5. 推荐的基础目录结构
月份 1 后续项目建议从一开始就按这个结构组织:
app/
├── __init__.py
├── config.py
├── models.py
├── services/
├── clients/
└── tools/
tests/
说明:
config.py:读取环境变量和配置models.py:放 Pydantic 模型或基础类型services/:核心业务逻辑clients/:外部服务调用封装,比如 LLM APItools/:工具调用相关逻辑tests/:测试
6. 为什么要尽早拆出 services
因为月份 1 最终项目要求:
- CLI 和 FastAPI 共用核心逻辑
- 核心逻辑不能写死在入口文件里
所以你必须尽早习惯这种结构:
- 入口层负责接收输入和输出结果
- service 层负责做真正业务逻辑
7. 实操任务
创建以下目录和文件:
demo_project/
├── main.py
├── utils.py
└── messages.py
要求:
- 在
messages.py里定义一个简单类或函数。 - 在
utils.py里定义一个字符串处理函数。 - 在
main.py中导入它们并调用。
8. 自测题
- 函数、模块、包的粒度分别是什么?
- 为什么不能把 CLI 逻辑和核心业务逻辑写在一起?
- 为什么项目里不推荐
from x import *?
9. 作业与验收
作业:
- 把你的
python_basics.py重构成至少 2 个模块。
验收标准:
- 文件职责清晰
import能正常工作- 你能说明每个模块负责什么
10. 常见错误
- 使用循环导入
- 文件名和标准库重名,例如
json.py - 把所有函数都塞进
utils.py
11. 延伸阅读
- Python 官方文档中的 modules 章节
- Real Python 的 packages 教程
12. 本章与前文关系
上一章解决的是“Python 语法怎么写”;这一章解决的是“代码写出来后,应该怎么组织”。这是从“会写一段脚本”走向“能维护一个小项目”的关键一步。
如果没有这一章,后面你在月份 1 里看到的所有结构都会显得很突然:
app/models.pyapp/config.pyapp/services/chat_service.pytests/test_models.py
实际上,这些都只是本章“函数、模块、包”三层组织方式的工程化延伸。
13. 本章在研发助手项目中的位置
研发助手项目最终会包含:
- CLI 入口
- FastAPI 入口
- service 层
- client 层
- tools 层
这些都不是新语法,而是模块和包的组织结果。你现在学的内容,会直接决定第 4 周项目是否清晰、是否能复用。
14. 为什么“模块拆分”对初学者反而更重要
有些人觉得:我刚开始学,先把所有东西写在一个文件里,不是更简单吗?
短期看似是这样,但一旦进入 AI 应用开发,这种方式很快会让你碰到三个问题:
第一,职责混在一起
你会在同一个文件里同时看到:
- 配置读取
- 模型请求
- 数据模型
- CLI 入口
- 错误处理
这种代码一开始可能还能看懂,过几天之后就会迅速失控。
第二,难以测试
如果每个函数都和全局状态、入口代码混在一起,后面写 pytest 会很痛苦。
第三,无法复用
第 4 周要求 CLI 和 FastAPI 共用核心逻辑。如果现在不学会拆分,后面只能复制两份代码。
15. 错误示例 vs 正确示例
错误示例:一个文件塞下所有东西
import os
def normalize_text(text: str) -> str:
return text.strip()
class ChatMessage:
def __init__(self, role: str, content: str) -> None:
self.role = role
self.content = content
def main() -> None:
message = ChatMessage(role="user", content=" hello ")
print(normalize_text(message.content))
if __name__ == "__main__":
main()
这段代码不是错在“不能运行”,而是它让数据模型、工具函数、入口函数都混在了一起。后面只要再加一点点逻辑,这个文件就会迅速膨胀。
正确示例:按职责拆分
demo_project/
├── main.py
├── messages.py
└── utils.py
这让你一眼就能判断:
- 哪个文件负责数据结构
- 哪个文件负责通用函数
- 哪个文件负责程序入口
16. 完整文件级示例:messages.py + utils.py + main.py
messages.py
"""定义消息相关的数据结构。"""
class ChatMessage:
"""最小消息对象。
当前阶段先用普通类表达结构,
后面会升级为数据类,再升级为 Pydantic 模型。
"""
def __init__(self, role: str, content: str) -> None:
self.role = role
self.content = content
def summary(self) -> str:
"""返回便于打印的摘要。"""
return f"[{self.role}] {self.content}"
utils.py
"""定义可复用的小工具函数。"""
def normalize_text(text: str) -> str:
"""清理首尾空白。
这种纯函数很适合后续写单元测试。
"""
return text.strip()
main.py
"""程序入口文件。"""
from messages import ChatMessage
from utils import normalize_text
def main() -> None:
message = ChatMessage(role="user", content=" hello month1 ")
cleaned = normalize_text(message.content)
print(message.summary())
print(cleaned)
if __name__ == "__main__":
main()
17. 逐段解释这组完整示例
messages.py 为什么单独存在
因为“消息”是一个稳定概念,它会在月份 1 里反复出现。后面从普通类升级到 Pydantic 时,只需要调整这个文件的实现,而不需要把整个项目翻一遍。
utils.py 为什么适合放纯函数
纯函数的特点是:
- 输入明确
- 输出明确
- 不依赖太多外部状态
这种代码:
- 容易测试
- 容易复用
- 容易理解
main.py 为什么只保留入口逻辑
入口文件的目标应该是“让读者一眼看到程序怎么启动”,而不是把全部细节都埋进去。
18. 从最小模块拆分到工程目录的递进
你可以把本章理解成三层递进:
第一层:单文件
适合练语法,不适合做项目。
第二层:多模块
适合形成最小的职责边界。
第三层:多包结构
适合进入真实工程,例如:
app/
├── api/
├── clients/
├── services/
└── tools/
月份 1 的后续项目会逐步走到第三层,但基础认知从本章开始。
19. 一个更接近后续项目的增强版目录
当你从 demo_project 继续升级时,可以逐步演化成:
app/
├── __init__.py
├── config.py
├── models.py
├── services/
│ └── chat_service.py
├── clients/
│ └── llm_client.py
└── tools/
└── registry.py
这不是让你现在就全部实现,而是让你提前知道:后面的目录结构不是凭空冒出来的,它是从“函数 -> 模块 -> 包”的自然延伸。
20. 为什么月份 1 要尽早建立 service 思维
你后面会经常看到一句话:CLI 和 API 必须共用核心逻辑。
这句话落到代码上,通常就意味着:
- CLI 只负责解析输入
- API 只负责处理 HTTP 协议
- 真正业务逻辑放在
service层
如果你没有模块边界意识,就很难真正理解这句话。
21. 调试与排错:本章最常见的 3 类问题
问题一:导入失败
常见现象:
ModuleNotFoundError
常见原因:
- 当前工作目录不对
- 文件名写错
- 模块路径写错
问题二:文件名和标准库重名
例如你把文件命名为 json.py、typing.py,会导致导入行为混乱。
问题三:utils.py 变成垃圾桶
初学者很容易把所有“暂时不知道放哪儿的函数”都塞进 utils.py。短期凑合,长期会造成职责模糊。
22. 本章完成后你应该具备的能力
完成本章后,你应当做到:
- 能把一个单文件脚本拆成至少两个模块。
- 能解释函数、模块、包三者的粒度差异。
- 能说明为什么入口文件不应该承担所有逻辑。
- 能为后续
app/services结构建立直觉。
23. 如果你卡在这里,先回看哪几章
- Python 基础语法仍然不稳:回看 01-Python最小入门.md
- 环境目录总是混乱:回看 02-环境准备与学习方法.md
24. 从本章过渡到下一章的桥接说明
接下来进入 03-类-数据类-类型注解.md。
本章解决了“代码放在哪儿”的问题,下一章将解决“结构化数据如何表达”的问题。后面无论是 ChatMessage、ChatRequest 还是 ToolCallResult,本质都是结构化对象,因此你必须进一步理解类、数据类和类型注解。