第十二章:实战项目建议
本章学习目标
- 理解项目开发流程
- 掌握项目结构组织
- 学会编写测试用例
- 了解代码规范和文档
- 独立完成一个完整项目
12.1 Python 项目结构
12.1.1 为什么要使用项目结构
良好的项目结构可以:
- 代码组织清晰,易于维护
- 方便团队协作
- 易于测试和部署
- 符合 Python 社区惯例
12.1.2 典型项目结构
my_project/ # 项目根目录
├── src/ # 源代码目录
│ ├── __init__.py # 包初始化
│ ├── module1.py # 功能模块1
│ ├── module2.py # 功能模块2
│ └── sub_package/ # 子包
│ ├── __init__.py
│ └── module3.py
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── test_module1.py
│ └── test_module2.py
├── docs/ # 文档目录
├── scripts/ # 脚本目录
├── data/ # 数据目录
├── notebooks/ # Jupyter notebooks
├── requirements.txt # 依赖列表
├── setup.py # 安装配置
├── pyproject.toml # 现代项目配置
├── .gitignore # Git 忽略文件
├── .editorconfig # 编辑器配置
├── README.md # 项目说明
└── LICENSE # 许可证
12.1.3 源代码目录选择
# 方式1:src 目录(推荐)
src/
my_package/
__init__.py
# 方式2:直接使用包
my_package/
__init__.py
12.2 pyproject.toml 配置
12.2.1 完整配置示例
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my_project"
version = "1.0.0"
description = "项目描述"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "you@example.com"}
]
keywords = ["python", "example"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"requests>=2.28.0",
"numpy>=1.24.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=22.0.0",
"ruff>=0.1.0",
"mypy>=0.950",
]
test = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
]
docs = [
"sphinx>=6.0.0",
]
[project.scripts]
my_project = "my_project.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.black]
line-length = 88
target-version = ["py310"]
include = '\.pyi?$'
[tool.ruff]
line-length = 88
select = ["E", "F", "W", "I"]
ignore = ["E501"]
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_functions = "test_*"
12.3 虚拟环境和依赖管理
12.3.1 创建虚拟环境
# 使用 venv
python -m venv venv
# 激活(Windows)
venv\Scripts\activate
# 或 PowerShell
venv\Scripts\Activate.ps1
# 激活(macOS/Linux)
source venv/bin/activate
# 退出
deactivate
12.3.2 安装依赖
# 从 requirements.txt 安装
pip install -r requirements.txt
# 从 pyproject.toml 安装
pip install -e .
# 开发模式安装
pip install -e ".[dev]"
# 安装所有可选依赖
pip install -e ".[dev,test,docs]"
12.3.3 requirements.txt
# 核心依赖
requests>=2.28.0
numpy>=1.24.0
pandas>=2.0.0
# 开发依赖
pytest>=7.0.0
black>=22.0.0
ruff>=0.1.0
mypy>=0.950
12.4 完整项目示例:命令行 Todo 应用
12.4.1 项目结构
todo_app/
├── src/
│ └── todo/
│ ├── __init__.py
│ ├── models.py
│ ├── storage.py
│ └── cli.py
├── tests/
│ ├── __init__.py
│ └── test_todo.py
├── pyproject.toml
└── README.md
12.4.2 数据模型
# src/todo/models.py
"""数据模型"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class TodoItem:
"""待办事项"""
id: int
title: str
completed: bool = False
priority: Literal["low", "medium", "high"] = "medium"
created_at: datetime = field(default_factory=datetime.now)
completed_at: datetime | None = None
def complete(self) -> None:
"""标记为完成"""
self.completed = True
self.completed_at = datetime.now()
def uncomplete(self) -> None:
"""标记为未完成"""
self.completed = False
self.completed_at = None
def __str__(self) -> str:
status = "✓" if self.completed else "✗"
priority_symbol = {"low": "↓", "medium": "→", "high": "↑"}
return f"[{status}] {priority_symbol[self.priority}] {self.title}"
12.4.3 存储层
# src/todo/storage.py
"""存储层"""
import json
from pathlib import Path
from typing import Literal
from .models import TodoItem
class Storage:
"""JSON 文件存储"""
def __init__(self, filepath: str = "todos.json") -> None:
self.filepath = Path(filepath)
self._todos: dict[int, TodoItem] = {}
self._load()
def _load(self) -> None:
"""加载数据"""
if self.filepath.exists():
with open(self.filepath, "r", encoding="utf-8") as f:
data = json.load(f)
self._todos = {
int(k): TodoItem(**v) for k, v in data.items()
}
def _save(self) -> None:
"""保存数据"""
data = {str(k): v.__dict__ for k, v in self._todos.items()}
with open(self.filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def add(self, title: str, priority: str = "medium") -> TodoItem:
"""添加待办"""
todo_id = max(self._todos.keys(), default=0) + 1
todo = TodoItem(id=todo_id, title=title, priority=priority)
self._todos[todo_id] = todo
self._save()
return todo
def get(self, todo_id: int) -> TodoItem | None:
"""获取待办"""
return self._todos.get(todo_id)
def list(self, completed: bool | None = None) -> list[TodoItem]:
"""列出待办"""
todos = list(self._todos.values())
if completed is not None:
todos = [t for t in todos if t.completed == completed]
return sorted(todos, key=lambda t: (t.completed, t.created_at))
def complete(self, todo_id: int) -> bool:
"""完成待办"""
todo = self._todos.get(todo_id)
if todo:
todo.complete()
self._save()
return True
return False
def uncomplete(self, todo_id: int) -> bool:
"""取消完成"""
todo = self._todos.get(todo_id)
if todo:
todo.uncomplete()
self._save()
return True
return False
def delete(self, todo_id: int) -> bool:
"""删除待办"""
if todo_id in self._todos:
del self._todos[todo_id]
self._save()
return True
return False
12.4.4 命令行接口
# src/todo/cli.py
"""命令行接口"""
import sys
from typing import Literal
from .models import TodoItem
from .storage import Storage
def print_header() -> None:
print("=" * 50)
print(" 待办事项管理器")
print("=" * 50)
def print_todos(todos: list[TodoItem]) -> None:
if not todos:
print("暂无待办事项")
return
for todo in todos:
print(f" {todo.id:2d}. {todo}")
print(f"\n共 {len(todos)} 项")
def add_todo(storage: Storage, title: str, priority: str) -> None:
"""添加待办"""
todo = storage.add(title, priority)
print(f"✓ 已添加: {todo.title}")
def list_todos(storage: Storage, filter_completed: bool | None) -> None:
"""列出待办"""
todos = storage.list(filter_completed)
print_todos(todos)
def complete_todo(storage: Storage, todo_id: int) -> None:
"""完成待办"""
if storage.complete(todo_id):
print(f"✓ 已完成待办 #{todo_id}")
else:
print(f"✗ 未找到待办 #{todo_id}")
def delete_todo(storage: Storage, todo_id: int) -> None:
"""删除待办"""
if storage.delete(todo_id):
print(f"✓ 已删除待办 #{todo_id}")
else:
print(f"✗ 未找到待办 #{todo_id}")
def main() -> None:
"""主函数"""
storage = Storage()
if len(sys.argv) < 2:
print_header()
print("用法: todo <命令> [参数]")
print("命令:")
print(" add <标题> [--priority low|medium|high]")
print(" ls [--completed] [--pending]")
print(" done <ID>")
print(" rm <ID>")
return
command = sys.argv[1]
match command:
case "add":
if len(sys.argv) < 3:
print("请输入待办标题")
return
title = sys.argv[2]
priority = "medium"
if "--priority" in sys.argv:
idx = sys.argv.index("--priority")
if idx + 1 < len(sys.argv):
priority = sys.argv[idx + 1]
add_todo(storage, title, priority)
case "ls" | "list":
completed: bool | None = None
if "--completed" in sys.argv:
completed = True
elif "--pending" in sys.argv:
completed = False
list_todos(storage, completed)
case "done" | "complete":
if len(sys.argv) < 3:
print("请输入待办 ID")
return
try:
todo_id = int(sys.argv[2])
complete_todo(storage, todo_id)
except ValueError:
print("无效的 ID")
case "rm" | "delete":
if len(sys.argv) < 3:
print("请输入待办 ID")
return
try:
todo_id = int(sys.argv[2])
delete_todo(storage, todo_id)
except ValueError:
print("无效的 ID")
case _:
print(f"未知命令: {command}")
if __name__ == "__main__":
main()
12.4.5 包初始化
# src/todo/__init__.py
"""Todo 应用包"""
from .models import TodoItem
from .storage import Storage
__version__ = "1.0.0"
__all__ = ["TodoItem", "Storage"]
12.4.6 测试用例
# tests/test_todo.py
"""测试用例"""
import pytest
from datetime import datetime
from todo.models import TodoItem
from todo.storage import Storage
@pytest.fixture
def storage(tmp_path):
"""创建临时存储"""
filepath = tmp_path / "todos.json"
return Storage(filepath=str(filepath))
def test_add_todo(storage):
"""测试添加待办"""
todo = storage.add("学习 Python", "high")
assert todo.id == 1
assert todo.title == "学习 Python"
assert todo.priority == "high"
assert todo.completed is False
def test_list_todos(storage):
"""测试列出待办"""
storage.add("任务1")
storage.add("任务2")
todos = storage.list()
assert len(todos) == 2
def test_complete_todo(storage):
"""测试完成待办"""
todo = storage.add("任务")
assert todo.completed is False
storage.complete(todo.id)
updated = storage.get(todo.id)
assert updated.completed is True
assert updated.completed_at is not None
def test_delete_todo(storage):
"""测试删除待办"""
todo = storage.add("任务")
result = storage.delete(todo.id)
assert result is True
assert storage.get(todo.id) is None
def test_empty_list(storage):
"""测试空列表"""
todos = storage.list()
assert len(todos) == 0
12.4.7 pyproject.toml
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "todo-app"
version = "1.0.0"
description = "命令行待办事项管理器"
requires-python = ">=3.10"
dependencies = []
[project.scripts]
todo = "todo.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
12.5 代码规范工具
12.5.1 Ruff(推荐)
# 安装
pip install ruff
# 检查代码
ruff check .
# 自动修复
ruff check . --fix
# 格式化代码
ruff format .
# 创建配置
ruff init
12.5.2 Black
# 安装
pip install black
# 格式化
black .
# 检查(不修改)
black --check .
# 自定义行长度
black --line-length 100 .
12.5.3 mypy
# 安装
pip install mypy
# 类型检查
mypy src/
# 严格模式
mypy src/ --strict
12.5.4 pytest
# 安装
pip install pytest pytest-cov
# 运行测试
pytest
# 带覆盖率
pytest --cov=src --cov-report=html
# 运行特定测试
pytest tests/test_todo.py::test_add_todo
12.6 Git 使用
12.6.1 基本命令
# 初始化仓库
git init
# 克隆仓库
git clone https://github.com/user/repo.git
# 添加文件
git add .
git add file.py
# 提交
git commit -m "feat: 添加新功能"
# 推送
git push origin main
# 拉取
git pull origin main
# 查看状态
git status
# 查看差异
git diff
12.6.2 .gitignore 示例
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# 虚拟环境
venv/
ENV/
env/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
# 测试
.pytest_cache/
.coverage
htmlcov/
.tox/
# 环境变量
.env
.env.local
# 日志
*.log
# 数据文件
*.json
!example.json
todos.json
12.7 项目建议汇总
初学者项目(1-2周)
| 项目 | 难度 | 涉及内容 |
|---|---|---|
| 命令行计算器 | ⭐ | 函数、控制流 |
| 待办事项管理器 | ⭐ | 文件 I/O、dataclass、CLI |
| 猜数字游戏 | ⭐ | 随机数、循环 |
| 文本统计工具 | ⭐⭐ | 文件处理、正则、字典 |
| 通讯录管理 | ⭐⭐ | CRUD、JSON、类 |
中级项目(2-4周)
| 项目 | 难度 | 涉及内容 |
|---|---|---|
| 天气查询 CLI | ⭐⭐ | HTTP 请求、API、JSON |
| RSS 阅读器 | ⭐⭐ | XML/JSON、网络、缓存 |
| 密码生成器 | ⭐⭐ | 随机数、加密、CLI |
| Markdown 转 HTML | ⭐⭐ | 正则、文件处理 |
| 简易爬虫 | ⭐⭐⭐ | HTTP、BeautifulSoup |
高级项目(4周以上)
| 项目 | 难度 | 涉及内容 |
|---|---|---|
| REST API 服务 | ⭐⭐⭐ | FastAPI、数据库、认证 |
| 图片批量处理 | ⭐⭐⭐ | Pillow、多线程 |
| 博客系统 | ⭐⭐⭐ | Web、数据库、模板 |
| 任务调度器 | ⭐⭐⭐ | 定时任务、通知 |
继续学习建议
深入 Python
- 官方文档:https://docs.python.org/3/
- PEP 规范:https://peps.python.org/
- Real Python:https://realpython.com/
Web 开发
- Flask:轻量级 Web 框架
- Django:全功能 Web 框架
- FastAPI:现代异步 Web 框架
数据科学
- NumPy:数值计算
- Pandas:数据分析
- Matplotlib:数据可视化
- Scikit-learn:机器学习
自动化
- Selenium:Web 自动化
- Playwright:现代自动化
- Scrapy:网页爬虫
测试
- pytest:主流测试框架
- unittest:标准库测试
- Coverage.py:代码覆盖率
课后练习
练习 12.1:完善 Todo 应用
- 添加优先级排序功能
- 添加搜索功能
- 添加截止日期功能
练习 12.2:创建新项目
- 设计项目结构
- 编写功能代码
- 编写测试用例
- 配置代码规范工具
练习 12.3:参与开源
- 找到一个感兴趣的开源项目
- 阅读代码和文档
- 提交第一个 Pull Request
本章小结
本章我们详细学习了项目开发:
项目结构:
- 标准的目录布局
- src 和 tests 分离
配置管理:
- pyproject.toml
- requirements.txt
- 虚拟环境
代码规范:
- Ruff、Black、mypy
- pytest 测试
版本控制:
- Git 基本使用
- .gitignore
实战项目:
- 完整的 Todo 应用
- 项目建议