第10章 10.1 项目结构与代码组织

——当代码从「能跑」到「能维护」,你只差这一步


上一章我们学了 dataclassattrs,学会了怎么用更少的代码定义「数据容器」。但你有没有这种感觉:学完新语法很开心,拿起之前的代码一看——天哪,我的 main.py 怎么有 800 行?

别慌,这不是你的问题。是你的代码还缺一个组织结构

今天我们要解决一个比「怎么写代码」更底层的问题:怎么让代码「住」进一个整齐的房子里


🎯 开场 3 分钟:为什么要学这个?

场景引入

想象你租了一个小公寓:
- 刚开始:一个行李箱,衣服随手塞,充电器缠在衣服堆里
- 住久了:找双袜子要翻遍整个箱子,早上出门找不到钥匙

代码也一样:
- 刚开始:一个 main.py,100 行,挺好
- 项目大了:一个 main.py,2000 行,from utils import xxx 到处引用,改一处坏三处

痛点来了

你可能遇到过这些「代码乱麻」时刻:

  1. 找不到东西:想改一个函数,翻了 10 个文件才找到
  2. 不敢动代码:加个功能,怕把别的功能搞坏
  3. 和别人合作:你改了你的 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()

配图1 - 配图1


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/ 测试代码 单元测试

配图2 - 配图2


🔥 实战 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 个核心点

  1. 项目结构是代码的「书架」:把相关的放一起,不同的拆开
  2. __init__.py 是包的身份证:让 Python 知道这是个包,不是普通文件夹
  3. dataclass + 分层结构 = 黄金搭档:数据归 models,逻辑归 services,入口统一调用

延伸学习资源

  1. Python 官方文档 - 模块(Modules)
  2. 《Python 编程:从入门到实践》- 第 10 章「文件和异常」
  3. Real Python - Python Modules and Packages

互动钩子

💬 你的第一个「正式项目」是什么?有没有过「代码找不到在哪」的抓狂时刻?评论区聊聊,下一章讲单元测试时,老粉优先回复!


下章预告:学会了怎么组织代码,下一章我们学怎么证明代码是对的——单元测试与 pytest,让你的代码「经得起折腾」。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。