← Back to 文字

函数、模块与包

本文来自《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 API
  • tools/:工具调用相关逻辑
  • tests/:测试

6. 为什么要尽早拆出 services

因为月份 1 最终项目要求:

  • CLI 和 FastAPI 共用核心逻辑
  • 核心逻辑不能写死在入口文件里

所以你必须尽早习惯这种结构:

  • 入口层负责接收输入和输出结果
  • service 层负责做真正业务逻辑

7. 实操任务

创建以下目录和文件:

demo_project/
├── main.py
├── utils.py
└── messages.py

要求:

  1. messages.py 里定义一个简单类或函数。
  2. utils.py 里定义一个字符串处理函数。
  3. main.py 中导入它们并调用。

8. 自测题

  1. 函数、模块、包的粒度分别是什么?
  2. 为什么不能把 CLI 逻辑和核心业务逻辑写在一起?
  3. 为什么项目里不推荐 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.py
  • app/config.py
  • app/services/chat_service.py
  • tests/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.pytyping.py,会导致导入行为混乱。

问题三:utils.py 变成垃圾桶

初学者很容易把所有“暂时不知道放哪儿的函数”都塞进 utils.py。短期凑合,长期会造成职责模糊。

22. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能把一个单文件脚本拆成至少两个模块。
  2. 能解释函数、模块、包三者的粒度差异。
  3. 能说明为什么入口文件不应该承担所有逻辑。
  4. 能为后续 app/services 结构建立直觉。

23. 如果你卡在这里,先回看哪几章

24. 从本章过渡到下一章的桥接说明

接下来进入 03-类-数据类-类型注解.md

本章解决了“代码放在哪儿”的问题,下一章将解决“结构化数据如何表达”的问题。后面无论是 ChatMessageChatRequest 还是 ToolCallResult,本质都是结构化对象,因此你必须进一步理解类、数据类和类型注解。

Fin