1.4 模块与包管理
引言:组织代码的艺术
小白理解:模块和包就像整理房间:
想象你的衣柜(项目):
- 不整理:所有衣服堆在一起,找东西要翻半天
- 用抽屉:上衣一个抽屉、裤子一个抽屉、袜子一个抽屉
在 Python 中:
- 不整理:所有代码写在一个文件,几千行代码找东西找死
- 用模块:API 调用放
api.py、工具放tools.py、配置放config.py模块 = 抽屉,包 = 衣柜!
想象你正在构建一个复杂的 AI Agent 系统,包含:
- LLM API 调用模块
- 工具(Tools)模块
- 提示词管理模块
- 数据处理模块
- 日志和监控模块
如果把所有代码都写在一个文件里,那将是维护的噩梦。这就是为什么我们需要模块化——将代码组织成独立的、可复用的模块。
学习目标
- ✅ 理解模块(Module)和包(Package)的区别
- ✅ 掌握 import 语句的各种形式
- ✅ 理解 __name__ 和 __main__ 的作用
- ✅ 构建规范的项目目录结构
- ✅ 实战:组织 Agent 项目代码
第一部分:模块基础
什么是模块?
模块就是一个包含 Python 代码的 .py 文件。
小白理解:
- 你写了一个文件
math_utils.py- 这个文件就是一个"模块"
- 其他文件可以"导入"这个模块,使用里面的函数
就像借工具:你有一套螺丝刀(模块),别人需要用的时候可以借(import)
创建 math_utils.py:
python
"""数学工具模块"""
def add(a: int, b: int) -> int:
"""加法"""
return a + b
def multiply(a: int, b: int) -> int:
"""乘法"""
return a * b
PI = 3.14159使用模块:
python
# main.py
import math_utils
result = math_utils.add(3, 5)
print(result) # 8
print(math_utils.PI) # 3.14159第二部分:import 的多种形式
小白理解 - import 的各种写法:
假设你要用别人的工具箱
math_utils:
写法 意思 用法 import math_utils借整个工具箱 math_utils.add(3, 5)import math_utils as mu借工具箱,起个外号 mu.add(3, 5)from math_utils import add只借螺丝刀 add(3, 5)from math_utils import *把工具箱里的都拿出来 add(3, 5)⚠️ 不推荐
1. 基本 import
python
import math_utils
result = math_utils.add(3, 5)2. import as(别名)
python
import math_utils as mu
result = mu.add(3, 5)3. from import(导入特定项)
python
from math_utils import add, PI
result = add(3, 5)
print(PI)4. from import as
python
from math_utils import add as addition
result = addition(3, 5)5. from import *(不推荐!)
python
from math_utils import *
# 问题:不知道导入了什么,可能产生命名冲突
result = add(3, 5)⚠️ 最佳实践:
- ✅ 明确导入:
from module import specific_function- ❌ 避免:
from module import *
第三部分:包(Package)
什么是包?
包是包含 __init__.py 文件的目录,可以包含多个模块。
小白理解 - 模块 vs 包:
概念 是什么 类比 模块 一个 .py文件一个抽屉 包 一个文件夹(含 __init__.py)一个衣柜(有多个抽屉) 模块:math_utils.py → 一个抽屉 包: my_agent/ → 一个衣柜 ├── __init__.py → 衣柜的"门"(必须有) ├── llm.py → 抽屉1 ├── tools.py → 抽屉2 └── utils/ → 衣柜里还有个小柜子 ├── __init__.py └── text.py
项目结构:
my_agent/
├── __init__.py
├── llm.py
├── tools.py
└── utils/
├── __init__.py
├── text.py
└── logging.py__init__.py 的作用
小白理解 -
__init__.py是什么?
__init__.py就像衣柜的**"门"和"目录"**:
- 没有它:Python 不认为这是一个包,没法 import
- 有了它:Python 知道"这是一个包,可以导入"
更重要的是,它还能控制对外暴露什么:
python# 用户只需要写: from my_agent import call_llm # 而不是: from my_agent.llm.openai_client import call_llm # 太长了!
python
# my_agent/__init__.py
"""AI Agent 包"""
from .llm import call_llm
from .tools import search_web, calculate
__version__ = "0.1.0"
__all__ = ["call_llm", "search_web", "calculate"]使用包:
python
import my_agent
# 可以直接使用
result = my_agent.call_llm("Hello")
# 或者
from my_agent import search_web第四部分:相对导入 vs 绝对导入
绝对导入(推荐)
python
# my_agent/tools.py
from my_agent.utils.text import clean_text
from my_agent.llm import call_llm相对导入
python
# my_agent/tools.py
from .utils.text import clean_text # 当前包的 utils.text
from .llm import call_llm # 当前包的 llm
from ..other_package import something # 上级包💡 最佳实践:
- 包内部使用相对导入
- 跨包使用绝对导入
第五部分:__name__ 和 __main__
小白理解 - 这是 Python 最常见的"魔法"之一!
你经常会看到这样的代码:
pythonif __name__ == "__main__": main()这是什么意思?简单说:只有直接运行这个文件时,才执行下面的代码
类比:想象你有一个瑞士军刀:
- 直接用(直接运行):展开所有工具
- 借给别人(被 import):只把需要的工具借出去
if __name__ == "__main__":就是判断"我是被直接用,还是被借出去"
理解 __name__ 变量
python
# my_module.py
print(f"__name__ 的值: {__name__}")
def main():
print("执行主函数")
if __name__ == "__main__":
main()运行效果:
bash
# 直接运行
$ python my_module.py
__name__ 的值: __main__
执行主函数
# 作为模块导入
$ python
>>> import my_module
__name__ 的值: my_module💡 应用场景:
- 模块既可以被导入使用
- 也可以直接运行测试
第六部分:实战 - Agent 项目结构
标准项目结构
ai_agent_project/
│
├── pyproject.toml # Poetry 配置
├── README.md # 项目说明
├── .gitignore # Git 忽略文件
├── .env # 环境变量(不提交)
│
├── src/ # 源代码目录
│ └── ai_agent/
│ ├── __init__.py
│ ├── agent.py # Agent 主逻辑
│ ├── llm/ # LLM 模块
│ │ ├── __init__.py
│ │ ├── openai_client.py
│ │ └── anthropic_client.py
│ ├── tools/ # Tools 模块
│ │ ├── __init__.py
│ │ ├── web_search.py
│ │ └── calculator.py
│ ├── prompts/ # 提示词模块
│ │ ├── __init__.py
│ │ └── templates.py
│ └── utils/ # 工具函数
│ ├── __init__.py
│ ├── logging.py
│ └── config.py
│
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── test_agent.py
│ └── test_tools.py
│
├── examples/ # 示例脚本
│ └── basic_usage.py
│
└── docs/ # 文档
└── api.md实现示例
1. llm/openai_client.py
python
"""OpenAI API 客户端"""
from typing import Optional
import os
def call_openai(
prompt: str,
model: str = "gpt-3.5-turbo",
api_key: Optional[str] = None
) -> str:
"""调用 OpenAI API"""
api_key = api_key or os.getenv("OPENAI_API_KEY")
# 实际实现...
return f"[OpenAI响应] {prompt}"2. tools/web_search.py
python
"""网络搜索工具"""
def search_web(query: str) -> str:
"""搜索网络"""
# 实际实现会调用搜索 API
return f"搜索结果: {query}"3. __init__.py
python
"""AI Agent 包"""
from .llm.openai_client import call_openai
from .tools.web_search import search_web
__version__ = "0.1.0"
__all__ = ["call_openai", "search_web"]4. 使用
python
# examples/basic_usage.py
from ai_agent import call_openai, search_web
if __name__ == "__main__":
response = call_openai("What is AI?")
print(response)
results = search_web("Python tutorials")
print(results)本节总结
核心概念
- 模块 = .py 文件
- 包 = 包含
__init__.py的目录 - 绝对导入 vs 相对导入
if __name__ == "__main__"模式- 规范的项目结构
核心概念一览表
| 概念 | 一句话解释 | 生活比喻 |
|---|---|---|
| 模块 | 一个 .py 文件 | 一个抽屉 |
| 包 | 包含 __init__.py 的文件夹 | 一个衣柜 |
| import | 导入模块使用其功能 | 借工具 |
__init__.py | 包的入口文件 | 衣柜的门和目录 |
__name__ | 模块的名字 | 身份证 |
__main__ | 直接运行时的特殊名字 | "我是本人" |
__all__ | 控制 from x import * 导出什么 | 清单 |
新手常见问题
Q1:为什么 import * 不推荐?
- 不知道导入了什么,可能和你的变量名冲突
- 代码可读性差,别人不知道函数从哪来的
Q2:__init__.py 可以是空的吗?
- 可以!空文件也能让 Python 识别这是一个包
- 但通常会在里面写一些初始化代码或导出公共 API
Q3:相对导入 . 和 .. 是什么意思?
.= 当前包(同级目录)..= 上级包(父目录)...= 上上级包
最佳实践
- ✅ 使用明确的 import
- ✅ 组织清晰的目录结构
- ✅ 在
__init__.py中暴露公共 API - ✅ 使用
__all__控制导出 - ✅ 模块和包名使用小写+下划线