第10章 10.1 项目结构与代码组织
——当代码从「能跑」到「能维护」,你只差这一步
上一章我们学了 dataclass 和 attrs,学会了怎么用更少的代码定义「数据容器」。但你有没有这种感觉:学完新语法很开心,拿起之前的代码一看——天哪,我的 main.py 怎么有 800 行?
别慌,这不是你的问题。是你的代码还缺一个组织结构。
今天我们要解决一个比「怎么写代码」更底层的问题:怎么让代码「住」进一个整齐的房子里。
🎯 开场 3 分钟:为什么要学这个?
场景引入
想象你租了一个小公寓:
- 刚开始:一个行李箱,衣服随手塞,充电器缠在衣服堆里
- 住久了:找双袜子要翻遍整个箱子,早上出门找不到钥匙
代码也一样:
- 刚开始:一个 main.py,100 行,挺好
- 项目大了:一个 main.py,2000 行,from utils import xxx 到处引用,改一处坏三处
痛点来了
你可能遇到过这些「代码乱麻」时刻:
- 找不到东西:想改一个函数,翻了 10 个文件才找到
- 不敢动代码:加个功能,怕把别的功能搞坏
- 和别人合作:你改了你的
utils.py,他改了他的utils.py,合并时哭了
学完这章能解决
学完这章,你能:
- 给自己的 Python 项目搭建一个清晰可扩展的结构
- 知道哪些代码该放一起,哪些该拆开
- 读别人的项目不再懵,能快速定位文件
🧱 基础 25 分钟:核心概念
10.1.1 项目结构是什么?
生活类比:
想象你去超市:
- 蔬菜区 → 生鲜区 → 调味品区 → 收银台
- 每个区放不同的东西,标识清楚,你闭着眼都能找到
项目结构也一样:把相关的代码放一起,给它起个清晰的名字。
10.1.2 一个最小可运行项目长什么样?
先看一个最简单的项目结构:
my_project/
├── main.py # 入口文件
├── config.py # 配置文件
└── utils.py # 工具函数
这三个文件各司其职:
config.py - 放配置,别到处硬编码:
# config.py
DATABASE_URL = "localhost:5432"
DEBUG = True
MAX_RETRY = 3
utils.py - 放工具函数,哪里用到哪里引:
# utils.py
def format_time(seconds):
"""把秒数转成「XX分XX秒」"""
minutes = seconds // 60
secs = seconds % 60
return f"{minutes}分{secs}秒"
def validate_email(email):
"""简单的邮箱验证"""
return "@" in email and "." in email.split("@")[1]
main.py - 程序的入口,调用其他模块:
# main.py
from config import DEBUG
from utils import format_time, validate_email
def main():
if DEBUG:
print("程序启动中...")
print(format_time(125)) # 输出:2分5秒
print(validate_email("test@example.com")) # 输出:True
if __name__ == "__main__":
main()
运行结果:
程序启动中...
2分5秒
True
一句话解释:入口文件 main.py 负责「组装」,配置和工具各自独立,哪里改都不影响全局。
10.1.3 当项目变大:包(Package)
痛点:项目有 50 个文件,全扔一起太乱了。
解决方案:用「文件夹」管理文件,Python 里叫包(Package)。
my_project/
├── main.py
├── config.py
├── models/ # 📁 一个包(文件夹)
│ ├── __init__.py
│ ├── user.py
│ └── order.py
└── services/ # 📁 另一个包
├── __init__.py
├── auth.py
└── payment.py
关键点:__init__.py 是 Python 包的「身份证」——有这个文件,Python 才认这是个包。
10.1.4 __init__.py 怎么用?
生活类比:__init__.py 像是超市的「楼层索引牌」,告诉别人这层卖什么。
基础用法:最简单的 __init__.py 可以是空的(只要有这个文件就行)。
进阶用法:用它来简化导入路径。
# services/__init__.py
# 方式1:只暴露 auth,不暴露 payment
from .auth import AuthService
# 方式2:定义 __all__,控制 from services import * 能导入什么
__all__ = ["AuthService"]
# main.py
# 之前:想用 AuthService 要写一长串
# from services.auth import AuthService
# 现在:有了 __init__.py,直接写
from services import AuthService
auth = AuthService()

10.1.5 相对导入 vs 绝对导入
痛点:包内部文件互相引用时,路径写错了,报 ImportError。
两种导入方式:
# 绝对导入(推荐新手)
from services.auth import AuthService # 从项目根目录开始找
from config import DEBUG # 同上
# 相对导入(包内部用)
from .auth import AuthService # 从当前文件所在目录开始找
from ..config import DEBUG # 从当前目录的上一级开始找
生活类比:
- 绝对导入 = 告诉别人「去北京市朝阳区建国路88号」
- 相对导入 = 告诉别人「去楼上左转第二个门」
什么时候用哪个:
- 模块内部互相引用 → 相对导入更简洁
- 入口文件引用模块 → 绝对导入更清晰
10.1.6 __name__ == "__main__" 是什么?
痛点:有时候想「直接运行这个文件测试」,但它又被其他文件导入,不想让它执行。
解决方案:
# auth.py
def login(username, password):
return username == "admin" and password == "123"
# 只有直接运行 auth.py 时才会执行下面这段
if __name__ == "__main__":
print(login("admin", "123")) # 测试用
运行 python auth.py → 输出 True(执行了测试代码)
在其他文件 import auth → 不会执行测试代码
一句话解释:__name__ 是 Python 的隐藏变量,直接运行的文件值是 "__main__",被导入的文件值是文件名。
10.1.7 一个标准项目结构模板
给一个真实项目常用的结构:
my_project/
├── main.py # 入口(启动程序)
├── requirements.txt # 依赖列表
├── config/
│ ├── __init__.py
│ ├── settings.py # 基本设置
│ └── secrets.py # 密钥(不上传 Git)
├── models/
│ ├── __init__.py
│ ├── user.py
│ └── order.py
├── services/
│ ├── __init__.py
│ ├── auth.py
│ └── payment.py
├── utils/
│ ├── __init__.py
│ ├── time_utils.py
│ └── validators.py
└── tests/ # 下一章会讲
├── __init__.py
└── test_auth.py
文件夹划分逻辑:
| 文件夹 | 放什么 | 举例 |
|---|---|---|
config/ |
配置、密钥 | 数据库地址、API 密钥 |
models/ |
数据模型 | User、Order(dataclass 正好用这里) |
services/ |
业务逻辑 | 登录逻辑、支付流程 |
utils/ |
工具函数 | 时间格式化、验证器 |
tests/ |
测试代码 | 单元测试 |

🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):搭建一个「记事本」项目结构
目标:学会创建标准项目骨架
完整代码:
# 目录结构创建脚本(一次性运行即可)
import os
def create_project_structure():
"""创建标准项目结构"""
dirs = ["config", "models", "services", "utils", "tests"]
for d in dirs:
os.makedirs(d, exist_ok=True)
# 每个目录都需要 __init__.py
open(f"{d}/__init__.py", "w").close()
print("✅ 项目结构创建完成!")
if __name__ == "__main__":
create_project_structure()
运行后你会看到:
✅ 项目结构创建完成!
预期输出:文件夹全部建好,每个文件夹里有 __init__.py
项目 2(15 分钟):读取 CSV 数据并展示
目标:用项目结构组织代码,读取真实数据
场景:你有一个 students.csv,想统计成绩
# students.csv(手动创建)
name,math,english
小明,85,92
小红,78,88
小刚,95,85
文件结构:
project_2/
├── main.py
├── config/
│ └── __init__.py
├── models/
│ ├── __init__.py
│ └── student.py
└── services/
├── __init__.py
└── analyzer.py
models/student.py:
from dataclasses import dataclass
@dataclass
class Student:
name: str
math: int
english: int
def average(self):
return (self.math + self.english) / 2
services/analyzer.py:
import csv
from models.student import Student
def load_students(filepath):
"""从 CSV 加载学生数据"""
students = []
with open(filepath, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
students.append(Student(
name=row["name"],
math=int(row["math"]),
english=int(row["english"])
))
return students
def find_top_student(students):
"""找平均分最高的学生"""
return max(students, key=lambda s: s.average())
main.py:
from services.analyzer import load_students, find_top_student
def main():
students = load_students("students.csv")
print("📊 学生成绩单:")
for s in students:
print(f" {s.name}: 数学{s.math}, 英语{s.english}, 平均{s.average()}")
top = find_top_student(students)
print(f"\n🏆 第一名:{top.name}(平均分 {top.average()})")
if __name__ == "__main__":
main()
运行结果:
📊 学生成绩单:
小明: 数学85, 英语92, 平均88.5
小红: 数学78, 英语88, 平均83.0
小刚: 数学95, 英语85, 平均90.0
🏆 第一名:小刚(平均分 90.0)
一句话解释:数据放 models、逻辑放 services、入口放 main.py,各司其职。
项目 3(15 分钟):命令行待办清单小工具
目标:组合多个模块,做一个有点真实用的小工具
功能:增删查待办事项,数据存 JSON 文件
文件结构:
todo_app/
├── main.py
├── config/
│ ├── __init__.py
│ └── settings.py
├── models/
│ ├── __init__.py
│ └── todo.py
├── services/
│ ├── __init__.py
│ └── todo_service.py
└── data/
└── todos.json
config/settings.py:
import os
# 获取当前文件所在目录的上层目录
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_FILE = os.path.join(BASE_DIR, "data", "todos.json")
models/todo.py:
from dataclasses import dataclass, asdict
from datetime import datetime
@dataclass
class Todo:
id: int
content: str
done: bool = False
created_at: str = ""
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().strftime("%Y-%m-%d %H:%M")
def to_dict(self):
return asdict(self)
services/todo_service.py:
import json
from models.todo import Todo
def load_todos(filepath):
"""从 JSON 文件加载待办"""
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
return [Todo(**item) for item in data]
except FileNotFoundError:
return []
def save_todos(filepath, todos):
"""保存待办到 JSON 文件"""
with open(filepath, "w", encoding="utf-8") as f:
json.dump([t.to_dict() for t in todos], f, ensure_ascii=False, indent=2)
def add_todo(todos, content):
"""添加新待办"""
new_id = max([t.id for t in todos], default=0) + 1
todo = Todo(id=new_id, content=content)
todos.append(todo)
return todo
def done_todo(todos, todo_id):
"""标记待办完成"""
for t in todos:
if t.id == todo_id:
t.done = True
return t
return None
def list_todos(todos, show_done=True):
"""列出待办"""
for t in todos:
if not show_done and t.done:
continue
status = "✅" if t.done else "⬜"
print(f" {status} [{t.id}] {t.content}")
main.py:
from config.settings import DATA_FILE
from services.todo_service import load_todos, save_todos, add_todo, done_todo, list_todos
def main():
todos = load_todos(DATA_FILE)
print("📝 欢迎使用待办清单!")
print("当前有", len([t for t in todos if not t.done]), "件待办\n")
# 添加几个示例
if not todos:
add_todo(todos, "完成 Python 项目结构练习")
add_todo(todos, "阅读下一章:单元测试")
save_todos(DATA_FILE, todos)
print("已添加示例待办:")
list_todos(todos)
# 模拟完成一个
print("\n🎯 模拟完成任务 1...")
done_todo(todos, 1)
save_todos(DATA_FILE, todos)
print("\n更新后的清单:")
list_todos(todos)
if __name__ == "__main__":
main()
运行结果:
📝 欢迎使用待办清单!
当前有 0 件待办
已添加示例待办:
⬜ [1] 完成 Python 项目结构练习
⬜ [2] 阅读下一章:单元测试
🎯 模拟完成任务 1...
更新后的清单:
✅ [1] 完成 Python 项目结构练习
⬜ [2] 阅读下一章:单元测试
一句话解释:数据模型用 dataclass、业务逻辑放 services、配置统一管理——这就是一个「正经项目」的样子。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:循环导入(Circular Import)
❌ 错误示例:
# a.py
from b import B # a 导入 b
class A: pass
# b.py
from a import A # b 又导入 a —— 死循环!
class B: pass
✅ 正确做法:
- 把共享的类放单独一个文件
- 或者在函数内部导入(延迟导入)
坑 2:__init__.py 里不要放太多东西
❌ 错误示例:
# __init__.py
import csv
import json
from .auth import AuthService
from .payment import PaymentService
# 这个文件越来越臃肿,难以维护
✅ 正确做法:
# __init__.py
# 只做简单的导出控制
from .auth import AuthService
from .payment import PaymentService
__all__ = ["AuthService", "PaymentService"]
坑 3:相对导入在主脚本里不工作
❌ 错误示例:
# main.py(同目录下的 services 目录)
from .services import AuthService # ❌ 报错!入口文件不能用相对导入
✅ 正确做法:
# main.py
from services import AuthService # ✅ 用绝对导入
坑 4:硬编码路径
❌ 错误示例:
# 写死了路径,只能在你电脑上跑
with open("C:/Users/xxx/data/todos.json") as f:
...
✅ 正确做法:
# 用相对路径或动态获取
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_FILE = os.path.join(BASE_DIR, "data", "todos.json")
坑 5:忘了 __init__.py
❌ 错误示例:
my_project/
├── services/
│ ├── auth.py # 没有 __init__.py
│ └── payment.py
✅ 正确做法:
my_project/
├── services/
│ ├── __init__.py # ✅ 空的也行,但必须有
│ ├── auth.py
│ └── payment.py
性能小贴士:减少导入开销
# 如果某个模块导入很慢,可以用延迟导入
def get_heavy_service():
import heavy_module # 只有调用时才导入
return heavy_module.HeavyService()
调试技巧:打印导入路径
import my_module
print(my_module.__file__) # 查看实际加载的是哪个文件
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):创建项目结构
- 输入:运行创建结构的代码
- 预期输出:看到 "✅ 项目结构创建完成!"
- 提示:直接复制项目 1 的代码运行
练习 2(2 分钟):添加一个工具函数
- 输入:在 utils.py 里添加 def greet(name): return f"你好,{name}!"
- 预期输出:能正常导入并调用
- 提示:新建 utils/utils.py 文件,写入函数
练习 3(2 分钟):修改 Student 模型
- 输入:在 models/student.py 里加一个 science 字段
- 预期输出:能处理新的 CSV 数据
- 提示:用 dataclass 的 @dataclass 装饰器,字段加 science: int
练习 4(2 分钟):给 Todo 加优先级
- 输入:在 models/todo.py 的 Todo 类里加 priority 字段(默认 0)
- 预期输出:能创建带优先级的待办
- 提示:priority: int = 0 加到字段定义里
练习 5(2 分钟):分析报错
- 输入:运行以下代码,看报什么错
# a.py
from b import ClassB
class ClassA: pass
# b.py
from a import ClassA
class ClassB: pass
- 预期输出:解释为什么报错,以及怎么修
- 提示:想想导入顺序
作业题(30 分钟 - 2 小时)
作业:做一个「个人账本」小工具
- 需求描述:记录日常收入和支出,统计本月花了多少钱
- 功能点:
1. 添加收支记录(类型、金额、备注、日期)
2. 查看所有记录
3. 统计支出总额
4. 数据存到 JSON 文件,下次打开还在 - 加分项:
1. 支持按月份筛选
2. 支持删除记录 - 验收标准:
- 能跑起来,不报错
- 添加记录后重启程序,数据还在
- 统计金额正确
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章 3 个核心点
- 项目结构是代码的「书架」:把相关的放一起,不同的拆开
__init__.py是包的身份证:让 Python 知道这是个包,不是普通文件夹- dataclass + 分层结构 = 黄金搭档:数据归
models,逻辑归services,入口统一调用
延伸学习资源
- Python 官方文档 - 模块(Modules)
- 《Python 编程:从入门到实践》- 第 10 章「文件和异常」
- Real Python - Python Modules and Packages
互动钩子
💬 你的第一个「正式项目」是什么?有没有过「代码找不到在哪」的抓狂时刻?评论区聊聊,下一章讲单元测试时,老粉优先回复!
下章预告:学会了怎么组织代码,下一章我们学怎么证明代码是对的——单元测试与 pytest,让你的代码「经得起折腾」。

评论(0)