feat(python进阶): 新增进阶综合项目课程

This commit is contained in:
zhiye.sun
2026-08-10 17:26:38 +08:00
parent aff05283a2
commit b348dc0a1f
7 changed files with 410 additions and 8 deletions

View File

@@ -0,0 +1,6 @@
# 运行完整示例时自动生成的本地任务数据。
task_data/
practice_data/
# Python 自动生成的缓存文件。
__pycache__/

View File

@@ -0,0 +1,167 @@
# 第 2-9 课Python 进阶综合项目——多文件任务管理程序
## 一、本课目标
完成本课后,你将能够把第二阶段所学内容组合成一个可运行的小程序:
1. 用多个 Python 文件组织程序;
2. 用 JSON 文件保存并恢复任务;
3. 用异常处理保护文件读取和用户输入;
4. 使用推导式、生成器、装饰器和类型注解;
5. 解释每个模块的职责,并完成第二阶段验收。
## 二、前置知识
需要完成或阅读过第二阶段第 1 至第 8 课。第 8 课虽然已跳过,但本项目不依赖第三方软件包,只使用 Python 标准库。
## 三、项目要解决什么问题
单个文件中的代码变多后,阅读和修改都会变困难。这个项目把任务管理功能拆分为三部分:
```text
main.py 程序入口:组织执行顺序、显示结果
task_service.py 业务功能:新增、完成、筛选和汇总任务
task_store.py 数据读写:把任务保存到 JSON 文件、再读回来
```
职责拆分的好处是:保存格式改变时主要修改 `task_store.py`;新增业务规则时主要修改 `task_service.py`;入口显示变化时主要修改 `main.py`
## 四、什么是 JSON
JSONJavaScript Object Notation是一种常用的文本数据格式。它能保存列表、字典、字符串、数字和布尔值适合本课保存任务数据。
Python 使用标准库 `json`
```python
json.dumps(data) # 把 Python 数据转换成 JSON 文本
json.loads(text) # 把 JSON 文本转换成 Python 数据
```
本项目保存的单条任务类似:
```python
{
"id": 1,
"title": "整理本课学习笔记",
"priority": "",
"completed": False,
}
```
## 五、完整示例
完整示例由 `main.py``task_service.py``task_store.py` 组成。请先逐个打开文件,再运行入口文件。
## 六、运行方法
在项目根目录执行:
```powershell
cd D:\Code\Python
python .\02_python进阶\2_9_python进阶综合项目\main.py
```
首次运行会自动创建:
```text
02_python进阶/2_9_python进阶综合项目/task_data/tasks.json
```
这是本地运行数据,已由 `.gitignore` 忽略。再次运行会继续读取已有任务,所以任务数量会增加,这是正常现象。
## 七、正常结果示例
第一次运行时,输出格式类似:
```text
读取到 0 条已有任务。
正在执行add_task
正在执行add_task
正在执行complete_task
本次新增的任务:
[1] 整理本课学习笔记|优先级:高|状态:已完成
[2] 运行任务管理程序|优先级:中|状态:未完成
未完成任务:
[2] 运行任务管理程序|优先级:中|状态:未完成
任务汇总:
全部2 条已完成1 条未完成1 条。
```
编号和总数量会因之前运行过的次数而不同。
## 八、关键代码解析
### 8.1 文件不存在时返回空列表
```python
if not data_file.exists():
return []
```
第一次运行还没有任务文件,这不是错误。返回空列表后,程序就可以从第一条任务开始添加。
### 8.2 装饰器记录调用
`@log_call` 会在原函数执行前输出函数名,但不会改变原函数的参数和返回值。它适合为多个关键操作增加相同的提示。
### 8.3 生成器逐条提供未完成任务
```python
for task in tasks:
if not task["completed"]:
yield task
```
`yield` 让函数每找到一条未完成任务就交出一条,不需要先创建完整的新列表。
### 8.4 类型注解
例如 `list[dict[str, object]]` 表示“由任务字典组成的列表”。任务中的值既可能是编号、文字,也可能是布尔值,所以这里使用 `object`
## 九、常见错误
### 9.1 直接运行 task_service.py
它是功能模块,不是入口程序。应运行 `main.py`
### 9.2 JSON 文件内容损坏
手工编辑 `tasks.json` 时漏写逗号或引号,会导致 JSON 格式错误。示例会安全返回空列表;练习中应捕获 `json.JSONDecodeError` 并给出提示。
### 9.3 忘记保存
列表只在程序运行期间存在。调用 `add_task()``complete_task()` 后,要调用 `save_tasks()` 才能写入文件。
### 9.4 使用 `wrapper()` 代替 `wrapper`
装饰器最后应 `return wrapper`,不是 `return wrapper()`;后者会在装饰阶段提前执行函数。
## 十、课堂练习
打开 `practice.py`,按六部分要求自己创建 `practice_store.py``practice_service.py``practice_main.py`。练习文件保持题面说明形式,不包含预置函数骨架。
## 十一、参考答案
本课不提前写入参考答案。完成后把三个练习文件保留在本课目录,我会按模块职责、功能正确性、可读性和知识掌握情况进行验证。
## 十二、本课小结
- 模块让不同职责的代码分开放置;
- JSON 能把 Python 的任务数据保存为文本;
- `Path` 让路径处理更安全清晰;
- `try...except` 让预期的读取问题不至于让程序崩溃;
- 推导式适合快速生成汇总数据,生成器适合逐条提供结果;
- 装饰器可为多个函数增加共同功能;
- 类型注解让函数接收什么、返回什么更清楚。
## 十三、验收标准
- 能独立运行完整示例并说明三个模块的职责;
- 能解释 JSON 保存和读取的方向;
- 能说明本项目中异常处理、推导式、生成器、装饰器和类型注解分别在哪里使用;
- 能按题目完成三个练习模块;
- 再次运行练习程序时,能够读取上次保存的任务;
- 已完成第二阶段的综合项目,具备进入第三阶段“面向对象编程”的基础。

View File

@@ -0,0 +1,52 @@
# 第 2-9 课完整示例:多文件任务管理程序入口
#
# 直接运行本文件会创建当前课程目录下的 task_data/tasks.json。
# 该文件是练习数据,已通过 .gitignore 排除,不会被提交到 Git。
from pathlib import Path
from task_service import add_task, build_task_report, complete_task, generate_pending_tasks
from task_store import load_tasks, save_tasks
LESSON_DIR = Path(__file__).parent
DATA_FILE = LESSON_DIR / "task_data" / "tasks.json"
def print_task(task: dict[str, object]) -> None:
"""按统一格式输出一条任务。"""
status_text = "已完成" if task["completed"] else "未完成"
print(f"[{task['id']}] {task['title']}|优先级:{task['priority']}|状态:{status_text}")
def main() -> None:
"""按照读取、修改、保存和汇总的顺序运行完整示例。"""
tasks = load_tasks(DATA_FILE)
print(f"读取到 {len(tasks)} 条已有任务。")
try:
first_task = add_task(tasks, "整理本课学习笔记", "")
second_task = add_task(tasks, "运行任务管理程序", "")
except ValueError as error:
# 本示例会传入合法标题;这里演示业务错误的安全处理方式。
print(f"添加任务失败:{error}")
return
complete_task(tasks, int(first_task["id"]))
save_tasks(DATA_FILE, tasks)
print("\n本次新增的任务:")
print_task(first_task)
print_task(second_task)
print("\n未完成任务:")
for task in generate_pending_tasks(tasks):
print_task(task)
report = build_task_report(tasks)
print("\n任务汇总:")
print(f"全部:{report['total']} 条,已完成:{report['completed']} 条,未完成:{report['pending']} 条。")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,74 @@
# 第 2-9 课课堂练习:扩展多文件任务管理程序
#
# 请先运行 main.py理解三个模块分别负责什么再完成下面的扩展。
# 本练习不预置导入、变量、函数或 pass请根据题目自己编写。
# 练习数据只能保存到当前课程目录的 practice_data 文件夹中。
import pathlib
# 第一部分:拆分模块职责
# 1. 新建 practice_store.py只放“读取和保存任务数据”的函数。
# 2. 新建 practice_service.py只放“新增、完成、查询任务”的函数。
# 3. 新建 practice_main.py作为程序入口从两个模块导入并调用函数。
# 4. 使用 pathlib.Path(__file__).parent 计算当前目录,不手工拼接路径。
LOCAL_PATH = pathlib.Path(__file__).parent
# 第二部分:保存与读取任务
# 1. 在 practice_store.py 中导入 json 和 Path。
# 2. 定义 load_tasks(data_file);文件不存在时 return []。
# 3. 使用 read_text(encoding="utf-8") 和 json.loads() 读取数据。
# 4. 捕获 FileNotFoundError 和 json.JSONDecodeError出现问题时返回 [] 并输出中文提示。
# 5. 定义 save_tasks(data_file, tasks);先创建父目录,再用 json.dumps() 保存。
# 6. 保存中文时使用 ensure_ascii=False 和 encoding="utf-8"。
# 第三部分:新增与完成任务
# 1. 在 practice_service.py 中定义 add_task(tasks, title, priority)。
# 2. title 去掉首尾空白后为空时,使用 raise ValueError("任务标题不能为空。")。
# 3. 新任务包含 id、title、priority、completed 四个键completed 初始为 False。
# 4. 用列表推导式取得已有 id并计算下一个 id空列表的第一个 id 是 1。
# 5. 定义 complete_task(tasks, task_id),找到任务后把 completed 设为 True。
# 6. 找到时 return True找不到时 return False。
# 第四部分:生成器与汇总
# 1. 定义 generate_pending_tasks(tasks) 生成器函数。
# 2. 只对 completed 为 False 的任务使用 yield。
# 3. 定义 build_task_report(tasks),返回全部、已完成、未完成数量组成的字典。
# 4. 已完成任务可以使用带 if 的列表推导式取得。
# 第五部分:增加调用日志
# 1. 从 functools 导入 wraps。
# 2. 定义 log_call(func) 装饰器,内部定义 wrapper(*args, **kwargs)。
# 3. wrapper 输出“正在执行:函数名”,调用原函数并 return 原结果。
# 4. 为 add_task() 和 complete_task() 添加 @log_call。
# 第六部分:入口程序
# 1. 在 practice_main.py 准备 practice_data/tasks.json 路径。
# 2. 读取已有任务后,新增两条任务:一条高优先级、一条中优先级。
# 3. 把第一条新增任务标记为完成,再保存全部任务。
# 4. 遍历生成器并输出未完成任务。
# 5. 输出汇总结果;再次运行时,能够继续读取上次保存的任务。
# 6. 捕获 ValueError 并输出中文错误提示,不让程序直接崩溃。
# 最终验收测试:
# 1. 三个模块职责清晰,入口文件不直接处理 JSON 细节;
# 2. 首次运行时能自动创建 practice_data/tasks.json
# 3. 再次运行时能读取已保存的任务;
# 4. 空标题不会添加任务,并给出中文提示;
# 5. 未完成任务通过包含 yield 的函数逐条获得;
# 6. 新增与完成任务时都会输出调用日志;
# 7. 汇总数量正确,已完成数和未完成数之和等于总数;
# 8. 不把 practice_data、__pycache__ 或虚拟环境提交到 Git。
# 完成后自查:
# 1. 是否能说明模块、函数和入口文件各自的职责;
# 2. 是否知道 JSON 文件为何要指定 UTF-8
# 3. 是否能解释 try...except 保护的是哪一段可能失败的代码;
# 4. 是否能解释列表推导式和生成器各自适合什么场景;
# 5. 是否知道装饰器在本项目中为函数增加了什么功能;
# 6. 是否写了类型注解,并让返回值与注解一致;
# 7. 是否只保存当前课程目录中的练习数据。

View File

@@ -0,0 +1,69 @@
# 第 2-9 课完整示例:任务业务功能模块
#
# 本模块使用字典表示任务。面向对象的“类”会在第三阶段学习。
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"正在执行:{func.__name__}")
return func(*args, **kwargs)
return wrapper
def get_next_task_id(tasks: list[dict[str, object]]) -> int:
"""根据已有任务计算下一个可用编号。"""
if not tasks:
return 1
# 列表推导式只取出编号max() 找到其中最大的编号。
task_ids = [int(task["id"]) for task in tasks]
return max(task_ids) + 1
@log_call
def add_task(tasks, title: str, priority: str) -> dict[str, object]:
"""创建一条未完成任务,添加到列表并返回这条新任务。"""
cleaned_title = title.strip()
if not cleaned_title:
raise ValueError("任务标题不能为空。")
task = {
"id": get_next_task_id(tasks),
"title": cleaned_title,
"priority": priority,
"completed": False,
}
tasks.append(task)
return task
@log_call
def complete_task(tasks: list[dict[str, object]], task_id: int) -> bool:
"""按编号把任务标记为完成;找到任务返回 True否则返回 False。"""
for task in tasks:
if task["id"] == task_id:
task["completed"] = True
return True
return False
def generate_pending_tasks(tasks: list[dict[str, object]]):
"""逐条生成未完成任务,不预先创建新的完整列表。"""
for task in tasks:
if not task["completed"]:
yield task
def build_task_report(tasks: list[dict[str, object]]) -> dict[str, object]:
"""汇总全部、已完成和未完成任务数量。"""
completed_tasks = [task for task in tasks if task["completed"]]
return {
"total": len(tasks),
"completed": len(completed_tasks),
"pending": len(tasks) - len(completed_tasks),
}

View File

@@ -0,0 +1,33 @@
# 第 2-9 课完整示例:任务数据读写模块
#
# 本模块只负责把任务列表保存到文件,或从文件读取任务列表。
import json
from pathlib import Path
def load_tasks(data_file: Path) -> list[dict[str, object]]:
"""从 JSON 文件读取任务列表;文件不存在或内容有误时返回空列表。"""
if not data_file.exists():
return []
try:
content = data_file.read_text(encoding="utf-8")
tasks = json.loads(content)
except (OSError, json.JSONDecodeError):
# 读取失败或 JSON 格式不正确时,给出安全的空列表,避免程序崩溃。
return []
# JSON 可以保存多种数据。这里只接受列表,避免后续遍历时出现意外错误。
if not isinstance(tasks, list):
return []
return tasks
def save_tasks(data_file: Path, tasks: list[dict[str, object]]) -> None:
"""把任务列表保存为 UTF-8 编码的 JSON 文件。"""
# 父目录不存在时先创建exist_ok=True 允许程序重复运行。
data_file.parent.mkdir(parents=True, exist_ok=True)
content = json.dumps(tasks, ensure_ascii=False, indent=2)
data_file.write_text(content, encoding="utf-8")