Compare commits
4 Commits
f7c820b0f4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a28c3b3168 | ||
|
|
f1d9548646 | ||
|
|
b348dc0a1f | ||
|
|
aff05283a2 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,3 +14,6 @@ venv/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# 数据库课程的本地 TOML 配置包含远程数据库账号和密码,不应提交。
|
||||||
|
04_数据库/**/config.toml
|
||||||
|
|||||||
2
02_python进阶/2_3_异常处理/.gitignore
vendored
Normal file
2
02_python进阶/2_3_异常处理/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
example_data/
|
||||||
|
practice_data/
|
||||||
341
02_python进阶/2_3_异常处理/README.md
Normal file
341
02_python进阶/2_3_异常处理/README.md
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
# 第 2-3 课:异常处理
|
||||||
|
|
||||||
|
## 一、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 解释异常是什么,以及为什么需要处理异常;
|
||||||
|
2. 阅读 Python 异常信息中的异常类型;
|
||||||
|
3. 使用 `try...except` 处理预期内的错误;
|
||||||
|
4. 分别处理 `ValueError`、`ZeroDivisionError` 和 `FileNotFoundError`;
|
||||||
|
5. 理解 `else` 和 `finally` 的执行时机;
|
||||||
|
6. 避免使用过大的 `try` 范围或空白 `except`;
|
||||||
|
7. 让文件读取和数据转换失败时给出清楚的中文提示。
|
||||||
|
|
||||||
|
## 二、前置知识
|
||||||
|
|
||||||
|
学习本课前,需要掌握:
|
||||||
|
|
||||||
|
- 条件判断、函数和返回值;
|
||||||
|
- `int()` 类型转换;
|
||||||
|
- 模块与程序入口判断;
|
||||||
|
- 使用 `Path` 读取和写入文本文件。
|
||||||
|
|
||||||
|
## 三、异常是什么
|
||||||
|
|
||||||
|
异常(Exception)是程序运行过程中发生的非正常情况。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```python
|
||||||
|
number = int("五")
|
||||||
|
```
|
||||||
|
|
||||||
|
运行后会看到类似信息:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ValueError: invalid literal for int() with base 10: '五'
|
||||||
|
```
|
||||||
|
|
||||||
|
中文解释:`int()` 需要可以转换为整数的内容,但字符串 `"五"` 不符合要求。
|
||||||
|
|
||||||
|
如果不处理异常,程序会在出错位置停止。异常处理可以让程序识别预期内的问题,给用户明确提示,并决定是否继续运行。
|
||||||
|
|
||||||
|
异常处理不是“隐藏所有错误”。我们只处理已经理解并且知道如何应对的异常。
|
||||||
|
|
||||||
|
## 四、使用 `try...except`
|
||||||
|
|
||||||
|
基本结构如下:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
number = int(number_text)
|
||||||
|
except ValueError:
|
||||||
|
print("请输入有效整数。")
|
||||||
|
```
|
||||||
|
|
||||||
|
执行顺序:
|
||||||
|
|
||||||
|
1. Python 先执行 `try` 中的代码;
|
||||||
|
2. 没有异常时,跳过 `except`;
|
||||||
|
3. 发生 `ValueError` 时,停止执行 `try` 中剩余代码;
|
||||||
|
4. 转到对应的 `except ValueError`;
|
||||||
|
5. `except` 执行结束后,程序可以继续向下运行。
|
||||||
|
|
||||||
|
## 五、只捕获明确的异常
|
||||||
|
|
||||||
|
本课会遇到三种常见异常。
|
||||||
|
|
||||||
|
### 5.1 `ValueError`
|
||||||
|
|
||||||
|
值错误(Value Error,`ValueError`)表示值的类型可能可以使用,但具体内容不符合要求:
|
||||||
|
|
||||||
|
```python
|
||||||
|
int("优秀")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 `ZeroDivisionError`
|
||||||
|
|
||||||
|
除零错误(Zero Division Error,`ZeroDivisionError`)表示程序尝试用数字除以零:
|
||||||
|
|
||||||
|
```python
|
||||||
|
average = 180 / 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 `FileNotFoundError`
|
||||||
|
|
||||||
|
文件未找到错误(File Not Found Error,`FileNotFoundError`)表示程序读取的路径不存在:
|
||||||
|
|
||||||
|
```python
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
```
|
||||||
|
|
||||||
|
不推荐写空白 `except`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
number = int(number_text)
|
||||||
|
except:
|
||||||
|
print("出错了。")
|
||||||
|
```
|
||||||
|
|
||||||
|
这种写法会把多种不同问题混在一起,用户不知道发生了什么,开发者也更难找到真正原因。
|
||||||
|
|
||||||
|
## 六、一个 `try` 对应多个 `except`
|
||||||
|
|
||||||
|
一段代码可能出现不同异常,可以分别处理:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
number = int(content.strip())
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("没有找到文件。")
|
||||||
|
except ValueError:
|
||||||
|
print("文件内容不是有效整数。")
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 会根据实际异常类型进入匹配的分支。清楚区分异常类型,才能给出准确提示。
|
||||||
|
|
||||||
|
## 七、`else` 的作用
|
||||||
|
|
||||||
|
`else` 只在 `try` 没有发生异常时执行:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
average = total_score / agent_count
|
||||||
|
except ZeroDivisionError:
|
||||||
|
print("Agent 数量不能为 0。")
|
||||||
|
else:
|
||||||
|
return average
|
||||||
|
```
|
||||||
|
|
||||||
|
这样可以让 `try` 只包含可能发生异常的操作,成功后的普通逻辑放到 `else`。
|
||||||
|
|
||||||
|
## 八、`finally` 的作用
|
||||||
|
|
||||||
|
`finally` 无论有没有发生异常都会执行:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("没有找到文件。")
|
||||||
|
finally:
|
||||||
|
print("文件读取结束。")
|
||||||
|
```
|
||||||
|
|
||||||
|
它常用于必须执行的收尾工作。本课用输出语句观察执行时机。上一课使用的 `with` 已经能帮助关闭文件,因此不要为了使用 `finally` 而放弃更清晰的 `with` 或 `Path.read_text()`。
|
||||||
|
|
||||||
|
## 九、异常与条件判断的区别
|
||||||
|
|
||||||
|
条件判断适合处理程序能够直接检查的业务规则:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if score < 0 or score > 100:
|
||||||
|
print("评分必须在 0 到 100 之间。")
|
||||||
|
```
|
||||||
|
|
||||||
|
异常处理适合处理操作执行时可能失败的情况:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
score = int(score_text)
|
||||||
|
except ValueError:
|
||||||
|
print("评分必须是整数。")
|
||||||
|
```
|
||||||
|
|
||||||
|
因此,评分转换失败使用 `except ValueError`,评分超出范围使用 `if`,两者各自负责不同问题。
|
||||||
|
|
||||||
|
## 十、缩小 `try` 的范围
|
||||||
|
|
||||||
|
推荐:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
number = int(number_text)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
print("转换完成。")
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把大量无关代码全部放进 `try`。范围过大时,即使其他代码产生同类型异常,也可能被误认为是预期问题。
|
||||||
|
|
||||||
|
## 十一、完整示例
|
||||||
|
|
||||||
|
本课示例文件是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
02_python进阶/2_3_异常处理/exception_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例依次演示:
|
||||||
|
|
||||||
|
1. 使用 `try...except` 处理整数转换失败;
|
||||||
|
2. 使用 `else` 返回成功的除法结果;
|
||||||
|
3. 分别处理正常文件和不存在的文件;
|
||||||
|
4. 使用 `finally` 输出每次读取结束提示。
|
||||||
|
|
||||||
|
示例只在本课 `example_data` 中创建文件,该目录已由 `.gitignore` 忽略。
|
||||||
|
|
||||||
|
## 十二、运行方法
|
||||||
|
|
||||||
|
在项目根目录运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_3_异常处理\exception_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成练习后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_3_异常处理\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十三、示例运行结果
|
||||||
|
|
||||||
|
```text
|
||||||
|
一、处理整数转换异常
|
||||||
|
“五”不是有效整数。
|
||||||
|
有效结果:5
|
||||||
|
无效结果:None
|
||||||
|
==============================
|
||||||
|
二、处理除以零异常
|
||||||
|
Agent 数量不能为 0。
|
||||||
|
正常结果:2.0
|
||||||
|
异常结果:None
|
||||||
|
==============================
|
||||||
|
三、处理文件读取异常
|
||||||
|
已完成对 agent_count.txt 的读取尝试。
|
||||||
|
没有找到文件:missing.txt
|
||||||
|
已完成对 missing.txt 的读取尝试。
|
||||||
|
文件中的 Agent 数量:3
|
||||||
|
不存在文件的读取结果:None
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十四、关键代码解析
|
||||||
|
|
||||||
|
### 14.1 异常类型写在 `except` 后
|
||||||
|
|
||||||
|
```python
|
||||||
|
except ValueError:
|
||||||
|
```
|
||||||
|
|
||||||
|
它表示这里只处理 `ValueError`。如果发生其他没有处理的异常,Python 仍会显示异常信息,帮助我们发现新的问题。
|
||||||
|
|
||||||
|
### 14.2 异常分支也要明确返回
|
||||||
|
|
||||||
|
如果函数成功时返回数字,失败时可以明确返回 `None`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
except ValueError:
|
||||||
|
print("请输入有效整数。")
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
调用者随后可以使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if result is not None:
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
注意不能简单写 `if result:`,因为合法数字 `0` 也会被当作假值。
|
||||||
|
|
||||||
|
### 14.3 `return` 后仍会执行 `finally`
|
||||||
|
|
||||||
|
即使 `try` 或 `except` 中执行了 `return`,函数真正结束前仍会执行 `finally`。可以运行示例观察这一点。
|
||||||
|
|
||||||
|
## 十五、常见错误
|
||||||
|
|
||||||
|
### 15.1 使用空白 `except`
|
||||||
|
|
||||||
|
空白 `except` 会捕获范围过广,让真正的程序错误难以发现。请写出明确异常类型。
|
||||||
|
|
||||||
|
### 15.2 捕获异常后什么都不做
|
||||||
|
|
||||||
|
不推荐:
|
||||||
|
|
||||||
|
```python
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
用户看不到提示,开发者也不知道数据为什么没有得到处理。
|
||||||
|
|
||||||
|
### 15.3 用异常代替全部条件判断
|
||||||
|
|
||||||
|
`try...except` 不能取代正常业务判断。数字是否在 0 到 100 之间仍应使用 `if` 检查。
|
||||||
|
|
||||||
|
### 15.4 把 `try` 包住整个程序
|
||||||
|
|
||||||
|
这会让异常来源变得模糊。应只包住可能发生且准备处理的操作。
|
||||||
|
|
||||||
|
### 15.5 把英文错误信息原样交给用户
|
||||||
|
|
||||||
|
学习时可以阅读英文异常类型,但面向用户的提示应说明他们可以如何修正输入。
|
||||||
|
|
||||||
|
## 十六、课堂练习
|
||||||
|
|
||||||
|
打开 `practice.py`,依次完成:
|
||||||
|
|
||||||
|
1. `parse_score(score_text)`:处理整数转换并检查评分范围;
|
||||||
|
2. `calculate_average(total_score, agent_count)`:处理除以零;
|
||||||
|
3. `read_score(file_path)`:处理不存在的评分文件;
|
||||||
|
4. `main()`:准备安全的练习数据并测试正常、异常分支。
|
||||||
|
|
||||||
|
练习题面已经明确给出了参数、返回值、输出文字、测试输入和预期结果。请先独立完成,不要直接复制示例函数。
|
||||||
|
|
||||||
|
## 十七、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。你完成后,我会从以下三个方面检查:
|
||||||
|
|
||||||
|
- 正确性:正常和异常分支是否符合预期;
|
||||||
|
- 可读性:`try` 范围、命名和中文提示是否清楚;
|
||||||
|
- 知识掌握:是否能正确选择异常类型,并区分异常处理与条件判断。
|
||||||
|
|
||||||
|
## 十八、本课小结
|
||||||
|
|
||||||
|
- 异常是程序运行时出现的非正常情况;
|
||||||
|
- `try` 放置可能发生异常的代码;
|
||||||
|
- `except` 处理明确类型的异常;
|
||||||
|
- `else` 只在没有异常时执行;
|
||||||
|
- `finally` 无论是否发生异常都会执行;
|
||||||
|
- `ValueError`、`ZeroDivisionError` 和 `FileNotFoundError` 对应不同问题;
|
||||||
|
- 异常处理用于应对失败,条件判断用于检查业务规则;
|
||||||
|
- 不要使用空白 `except` 隐藏问题。
|
||||||
|
|
||||||
|
## 十九、验收标准
|
||||||
|
|
||||||
|
- 能用自己的话解释异常处理的作用;
|
||||||
|
- 能看懂三个本课异常类型;
|
||||||
|
- 示例程序输出与讲义一致;
|
||||||
|
- 能针对不同错误使用明确的 `except`;
|
||||||
|
- 能说明 `else` 和 `finally` 的执行时机;
|
||||||
|
- `parse_score("0")` 和 `parse_score("100")` 均能返回合法结果;
|
||||||
|
- 非数字和越界评分会给出不同中文提示;
|
||||||
|
- 除数为零时程序不会崩溃;
|
||||||
|
- 文件不存在时程序不会崩溃;
|
||||||
|
- 练习只在 `practice_data` 中写入数据;
|
||||||
|
- 没有使用空白 `except` 或用 `pass` 隐藏异常。
|
||||||
90
02_python进阶/2_3_异常处理/exception_example.py
Normal file
90
02_python进阶/2_3_异常处理/exception_example.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 第 2-3 课完整示例:异常处理
|
||||||
|
#
|
||||||
|
# 本程序通过固定测试数据演示异常处理,不会要求用户输入。
|
||||||
|
# 文件示例只会读取当前课程目录中的 example_data 文件夹。
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
LESSON_DIR = Path(__file__).parent
|
||||||
|
DATA_DIR = LESSON_DIR / "example_data"
|
||||||
|
CONFIG_FILE = DATA_DIR / "agent_count.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_positive_integer(number_text):
|
||||||
|
"""把文本转换为正整数,失败时返回 None。"""
|
||||||
|
try:
|
||||||
|
number = int(number_text)
|
||||||
|
except ValueError:
|
||||||
|
# ValueError 表示值的内容不符合转换要求。
|
||||||
|
print(f"“{number_text}”不是有效整数。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if number <= 0:
|
||||||
|
print("数字必须大于 0。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return number
|
||||||
|
|
||||||
|
|
||||||
|
def divide_tool_count(tool_count, agent_count):
|
||||||
|
"""计算平均工具数量,除数为零时返回 None。"""
|
||||||
|
try:
|
||||||
|
average = tool_count / agent_count
|
||||||
|
except ZeroDivisionError:
|
||||||
|
# ZeroDivisionError 表示程序尝试用数字除以零。
|
||||||
|
print("Agent 数量不能为 0。")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# 只有 try 中没有发生异常时,才会执行 else。
|
||||||
|
return average
|
||||||
|
|
||||||
|
|
||||||
|
def read_agent_count(file_path):
|
||||||
|
"""读取文件中的 Agent 数量,并保证输出结束提示。"""
|
||||||
|
try:
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
return int(content.strip())
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"没有找到文件:{file_path.name}")
|
||||||
|
except ValueError:
|
||||||
|
print(f"{file_path.name} 中保存的不是有效整数。")
|
||||||
|
finally:
|
||||||
|
# 无论是否发生异常,finally 都会执行。
|
||||||
|
print(f"已完成对 {file_path.name} 的读取尝试。")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_example_file():
|
||||||
|
"""在课程专用目录中准备安全的示例文件。"""
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
CONFIG_FILE.write_text("3\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""依次运行整数转换、除法和文件读取示例。"""
|
||||||
|
print("一、处理整数转换异常")
|
||||||
|
valid_number = parse_positive_integer("5")
|
||||||
|
invalid_number = parse_positive_integer("五")
|
||||||
|
print(f"有效结果:{valid_number}")
|
||||||
|
print(f"无效结果:{invalid_number}")
|
||||||
|
|
||||||
|
print("=" * 30)
|
||||||
|
print("二、处理除以零异常")
|
||||||
|
average = divide_tool_count(6, 3)
|
||||||
|
failed_average = divide_tool_count(6, 0)
|
||||||
|
print(f"正常结果:{average}")
|
||||||
|
print(f"异常结果:{failed_average}")
|
||||||
|
|
||||||
|
print("=" * 30)
|
||||||
|
print("三、处理文件读取异常")
|
||||||
|
prepare_example_file()
|
||||||
|
agent_count = read_agent_count(CONFIG_FILE)
|
||||||
|
missing_count = read_agent_count(DATA_DIR / "missing.txt")
|
||||||
|
print(f"文件中的 Agent 数量:{agent_count}")
|
||||||
|
print(f"不存在文件的读取结果:{missing_count}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
58
02_python进阶/2_3_异常处理/practice.py
Normal file
58
02_python进阶/2_3_异常处理/practice.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# 第 2-3 课课堂练习:异常处理
|
||||||
|
#
|
||||||
|
# 请先阅读讲义并运行完整示例,再按照顺序完成练习。
|
||||||
|
# 不要删除题目、测试数据、预期结果和自查注释。
|
||||||
|
# 本练习只读取当前课程目录中的 practice_data 文件,不删除或覆盖其他文件。
|
||||||
|
|
||||||
|
# 第一部分:定义 parse_score(score_text) 函数
|
||||||
|
# 1. 参数 score_text 接收一个字符串。
|
||||||
|
# 2. 在 try 中使用 int(score_text) 转换为整数,并保存结果。
|
||||||
|
# 3. 捕获 ValueError,输出“评分必须是整数。”,然后 return None。
|
||||||
|
# 4. 转换成功后,检查评分是否处于 0 到 100 之间。
|
||||||
|
# 5. 超出范围时输出“评分必须在 0 到 100 之间。”,然后 return None。
|
||||||
|
# 6. 合法时 return 转换后的整数,不要在函数内输出合法结果。
|
||||||
|
# 7. 分别传入 "90"、"优秀"、"120",预期返回 90、None、None。
|
||||||
|
|
||||||
|
# 第二部分:定义 calculate_average(total_score, agent_count) 函数
|
||||||
|
# 1. 在 try 中计算 total_score / agent_count。
|
||||||
|
# 2. 捕获 ZeroDivisionError,输出“Agent 数量不能为 0。”并 return None。
|
||||||
|
# 3. 使用 else return 平均分。
|
||||||
|
# 4. 传入 180 和 2,预期返回 90.0。
|
||||||
|
# 5. 传入 180 和 0,预期返回 None,程序不能崩溃。
|
||||||
|
|
||||||
|
# 第三部分:定义 read_score(file_path) 函数
|
||||||
|
# 1. 在 try 中使用 read_text(encoding="utf-8") 读取文本。
|
||||||
|
# 2. 调用 strip() 清理文本,再调用 parse_score() 并 return 结果。
|
||||||
|
# 3. 捕获 FileNotFoundError,输出“没有找到评分文件。”并 return None。
|
||||||
|
# 4. 添加 finally,并输出“评分文件读取结束。”。
|
||||||
|
# 5. 注意:parse_score() 已经负责处理 ValueError,不要重复捕获同一错误。
|
||||||
|
|
||||||
|
# 第四部分:定义 main() 函数
|
||||||
|
# 1. 使用 mkdir(parents=True, exist_ok=True) 创建 PRACTICE_DIR。
|
||||||
|
# 2. 使用 write_text() 把 "90\n" 写入 SCORE_FILE,并指定 utf-8。
|
||||||
|
# 3. 调用 read_score(SCORE_FILE),把返回值保存为 saved_score。
|
||||||
|
# 4. saved_score 不是 None 时,输出“读取到的评分:90”。
|
||||||
|
# 5. 再调用 read_score(PRACTICE_DIR / "missing.txt") 测试文件不存在的分支。
|
||||||
|
# 6. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||||
|
|
||||||
|
# 最终验收测试:
|
||||||
|
# 1. parse_score("0") 返回 0;
|
||||||
|
# 2. parse_score("100") 返回 100;
|
||||||
|
# 3. parse_score("优秀") 输出提示并返回 None;
|
||||||
|
# 4. parse_score("-1") 和 parse_score("101") 均返回 None;
|
||||||
|
# 5. calculate_average(180, 2) 返回 90.0;
|
||||||
|
# 6. calculate_average(180, 0) 不崩溃并返回 None;
|
||||||
|
# 7. 存在的评分文件可以正常读取;
|
||||||
|
# 8. 不存在的文件会输出提示,finally 中的文字仍然出现;
|
||||||
|
# 9. 所有异常都捕获了明确类型,没有使用空白 except。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. try 中是否只放可能发生异常的代码;
|
||||||
|
# 2. 是否分别理解 ValueError、ZeroDivisionError 和 FileNotFoundError;
|
||||||
|
# 3. 出现异常后,程序是否给出中文提示而不是直接崩溃;
|
||||||
|
# 4. 是否使用 return 返回结果,而不是只在函数中 print();
|
||||||
|
# 5. 是否理解 else 只在没有异常时执行;
|
||||||
|
# 6. 是否理解 finally 无论有无异常都会执行;
|
||||||
|
# 7. 是否没有使用 except Exception 隐藏所有问题;
|
||||||
|
# 8. 是否没有删除题目和验收说明。
|
||||||
355
02_python进阶/2_4_推导式与简化写法/README.md
Normal file
355
02_python进阶/2_4_推导式与简化写法/README.md
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
# 第 2-4 课:推导式与简化写法
|
||||||
|
|
||||||
|
## 一、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 解释推导式是什么,以及它适合解决什么问题;
|
||||||
|
2. 使用列表推导式转换和筛选数据;
|
||||||
|
3. 使用字典推导式创建键值对应关系;
|
||||||
|
4. 使用集合推导式收集不重复的数据;
|
||||||
|
5. 使用简单的条件表达式返回二选一结果;
|
||||||
|
6. 在普通循环和推导式之间选择更清晰的写法;
|
||||||
|
7. 避免编写难以阅读的复杂推导式。
|
||||||
|
|
||||||
|
## 二、前置知识
|
||||||
|
|
||||||
|
学习本课前,需要掌握:
|
||||||
|
|
||||||
|
- 列表、字典和集合;
|
||||||
|
- `for` 循环和 `if` 条件判断;
|
||||||
|
- 函数、参数和返回值;
|
||||||
|
- 字典的 `get()` 方法。
|
||||||
|
|
||||||
|
## 三、推导式是什么
|
||||||
|
|
||||||
|
推导式(Comprehension)是 Python 根据可遍历数据快速创建新集合的一种写法。
|
||||||
|
|
||||||
|
第一阶段中,我们使用普通循环生成新列表:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_names = []
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
agent_names.append(agent["name"])
|
||||||
|
```
|
||||||
|
|
||||||
|
使用列表推导式可以写成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_names = [agent["name"] for agent in agents]
|
||||||
|
```
|
||||||
|
|
||||||
|
两段代码的结果相同。推导式没有改变原来的 `agents`,而是创建并返回一个新列表。
|
||||||
|
|
||||||
|
## 四、列表推导式的基本结构
|
||||||
|
|
||||||
|
```python
|
||||||
|
[需要保存的值 for 临时变量 in 可遍历数据]
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_names = [agent["name"] for agent in agents]
|
||||||
|
```
|
||||||
|
|
||||||
|
建议从右向左理解:
|
||||||
|
|
||||||
|
1. `for agent in agents`:依次取出每个 Agent;
|
||||||
|
2. `agent["name"]`:取得当前 Agent 的名称;
|
||||||
|
3. `[...]`:把每次得到的名称放入新列表。
|
||||||
|
|
||||||
|
推导式中的 `agent` 只是本次遍历使用的临时变量。
|
||||||
|
|
||||||
|
## 五、使用列表推导式筛选数据
|
||||||
|
|
||||||
|
普通循环写法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
enabled_names = []
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
if agent["enabled"]:
|
||||||
|
enabled_names.append(agent["name"])
|
||||||
|
```
|
||||||
|
|
||||||
|
列表推导式写法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
enabled_names = [
|
||||||
|
agent["name"]
|
||||||
|
for agent in agents
|
||||||
|
if agent["enabled"]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
带筛选条件的结构是:
|
||||||
|
|
||||||
|
```python
|
||||||
|
[需要保存的值 for 临时变量 in 可遍历数据 if 条件]
|
||||||
|
```
|
||||||
|
|
||||||
|
只有条件为 `True` 的数据才会进入新列表。
|
||||||
|
|
||||||
|
## 六、转换与筛选的区别
|
||||||
|
|
||||||
|
转换表示每条数据都参与,但保存为另一种形式:
|
||||||
|
|
||||||
|
```python
|
||||||
|
name_lengths = [len(agent["name"]) for agent in agents]
|
||||||
|
```
|
||||||
|
|
||||||
|
筛选表示只有符合条件的数据才保留:
|
||||||
|
|
||||||
|
```python
|
||||||
|
enabled_agents = [agent for agent in agents if agent["enabled"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
一个推导式也可以同时转换和筛选:
|
||||||
|
|
||||||
|
```python
|
||||||
|
enabled_names = [
|
||||||
|
agent["name"]
|
||||||
|
for agent in agents
|
||||||
|
if agent["enabled"]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 七、字典推导式
|
||||||
|
|
||||||
|
字典推导式(Dictionary Comprehension)用于创建新字典:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tool_counts = {
|
||||||
|
agent["name"]: len(agent.get("tools", []))
|
||||||
|
for agent in agents
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
基本结构:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{键: 值 for 临时变量 in 可遍历数据}
|
||||||
|
```
|
||||||
|
|
||||||
|
上述示例的结果类似:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"代码助手": 2,
|
||||||
|
"聊天助手": 1,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字典的键不能重复。如果多个数据生成同一个键,后生成的值会覆盖之前的值。因此,应先确认选作键的数据具有合适的唯一性。
|
||||||
|
|
||||||
|
## 八、集合推导式
|
||||||
|
|
||||||
|
集合推导式(Set Comprehension)用于创建不包含重复值的集合:
|
||||||
|
|
||||||
|
```python
|
||||||
|
models = {agent["model"] for agent in agents}
|
||||||
|
```
|
||||||
|
|
||||||
|
基本结构:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{需要保存的值 for 临时变量 in 可遍历数据}
|
||||||
|
```
|
||||||
|
|
||||||
|
集合与字典都使用花括号,但字典推导式包含 `键: 值`,集合推导式只有一个值。
|
||||||
|
|
||||||
|
如果每个 Agent 中还有一个工具列表,可以使用两层 `for`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
unique_tools = {
|
||||||
|
tool
|
||||||
|
for agent in agents
|
||||||
|
for tool in agent.get("tools", [])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
它等价于:
|
||||||
|
|
||||||
|
```python
|
||||||
|
unique_tools = set()
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
for tool in agent.get("tools", []):
|
||||||
|
unique_tools.add(tool)
|
||||||
|
```
|
||||||
|
|
||||||
|
本课只练习这一种容易理解的两层结构,不加入更多条件。
|
||||||
|
|
||||||
|
## 九、条件表达式
|
||||||
|
|
||||||
|
条件表达式(Conditional Expression)可以根据一个条件从两个值中选择一个:
|
||||||
|
|
||||||
|
```python
|
||||||
|
status_text = "启用" if enabled else "停用"
|
||||||
|
```
|
||||||
|
|
||||||
|
执行顺序:
|
||||||
|
|
||||||
|
1. 检查 `enabled`;
|
||||||
|
2. 条件为 `True`,得到 `"启用"`;
|
||||||
|
3. 条件为 `False`,得到 `"停用"`。
|
||||||
|
|
||||||
|
它等价于:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if enabled:
|
||||||
|
status_text = "启用"
|
||||||
|
else:
|
||||||
|
status_text = "停用"
|
||||||
|
```
|
||||||
|
|
||||||
|
条件表达式适合简单的二选一。如果每个分支需要执行多步操作,应继续使用普通 `if...else`。
|
||||||
|
|
||||||
|
## 十、筛选条件与条件表达式不要混淆
|
||||||
|
|
||||||
|
筛选只决定某条数据是否保留:
|
||||||
|
|
||||||
|
```python
|
||||||
|
[agent["name"] for agent in agents if agent["enabled"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
条件表达式会为每条数据选择一个结果:
|
||||||
|
|
||||||
|
```python
|
||||||
|
[
|
||||||
|
"启用" if agent["enabled"] else "停用"
|
||||||
|
for agent in agents
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
第一段结果数量可能减少,第二段会为每个 Agent 产生一个状态文字。
|
||||||
|
|
||||||
|
## 十一、什么时候使用普通循环
|
||||||
|
|
||||||
|
以下情况优先使用普通循环:
|
||||||
|
|
||||||
|
- 每次遍历需要执行多个步骤;
|
||||||
|
- 需要输出中间过程;
|
||||||
|
- 包含多层条件判断;
|
||||||
|
- 推导式换行后仍难以理解;
|
||||||
|
- 需要在循环中提前 `break` 或 `return`。
|
||||||
|
|
||||||
|
代码行数更少不代表代码更好。能够让自己和其他人快速理解的写法才更合适。
|
||||||
|
|
||||||
|
## 十二、完整示例
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
02_python进阶/2_4_推导式与简化写法/comprehension_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例会演示:
|
||||||
|
|
||||||
|
1. 普通循环与列表推导式得到相同结果;
|
||||||
|
2. 筛选已启用的 Agent 名称;
|
||||||
|
3. 创建名称到工具数量的字典;
|
||||||
|
4. 收集所有不重复工具;
|
||||||
|
5. 使用条件表达式显示 Agent 状态。
|
||||||
|
|
||||||
|
## 十三、运行方法
|
||||||
|
|
||||||
|
在项目根目录运行示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_4_推导式与简化写法\comprehension_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成练习后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_4_推导式与简化写法\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十四、预期结果
|
||||||
|
|
||||||
|
```text
|
||||||
|
普通循环结果:['代码助手', '测试助手']
|
||||||
|
列表推导式结果:['代码助手', '测试助手']
|
||||||
|
工具数量字典:{'代码助手': 2, '聊天助手': 1, '测试助手': 2}
|
||||||
|
不重复工具:['搜索', '测试', '终端']
|
||||||
|
Agent 状态:
|
||||||
|
- 代码助手:启用
|
||||||
|
- 聊天助手:停用
|
||||||
|
- 测试助手:启用
|
||||||
|
```
|
||||||
|
|
||||||
|
集合没有固定顺序。示例使用 `sorted()` 只是为了让输出顺序稳定,便于核对;集合本身仍然不保证顺序。
|
||||||
|
|
||||||
|
## 十五、常见错误
|
||||||
|
|
||||||
|
### 15.1 忘记最外层括号
|
||||||
|
|
||||||
|
列表推导式使用方括号,字典和集合推导式使用花括号。
|
||||||
|
|
||||||
|
### 15.2 把筛选条件写到错误位置
|
||||||
|
|
||||||
|
筛选条件写在 `for...in...` 后面:
|
||||||
|
|
||||||
|
```python
|
||||||
|
[agent["name"] for agent in agents if agent["enabled"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15.3 混淆字典和集合推导式
|
||||||
|
|
||||||
|
```python
|
||||||
|
{agent["name"] for agent in agents}
|
||||||
|
```
|
||||||
|
|
||||||
|
这是集合,因为没有 `键: 值`。创建字典必须写冒号。
|
||||||
|
|
||||||
|
### 15.4 推导式过于复杂
|
||||||
|
|
||||||
|
不要在一个推导式中堆叠多层条件表达式。看不懂时应立即改回普通循环。
|
||||||
|
|
||||||
|
### 15.5 误以为推导式会修改原数据
|
||||||
|
|
||||||
|
推导式通常创建新列表、新字典或新集合。除非其中主动修改对象,否则原集合不会因为推导式本身被替换。
|
||||||
|
|
||||||
|
## 十六、课堂练习
|
||||||
|
|
||||||
|
打开 `practice.py`,依次完成:
|
||||||
|
|
||||||
|
1. 创建全部 Agent 名称列表;
|
||||||
|
2. 筛选已启用 Agent 的名称;
|
||||||
|
3. 创建名称到模型的字典;
|
||||||
|
4. 收集不重复工具;
|
||||||
|
5. 使用条件表达式返回状态文字;
|
||||||
|
6. 在 `main()` 中保存并输出所有返回结果。
|
||||||
|
|
||||||
|
题目已经给出参数、返回值、示例数据和预期结果。请先独立完成。
|
||||||
|
|
||||||
|
## 十七、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。你完成后,我会从正确性、可读性和知识掌握情况三个方面验证,并重点检查你是否能够解释推导式,而不只是记住格式。
|
||||||
|
|
||||||
|
## 十八、本课小结
|
||||||
|
|
||||||
|
- 推导式可以根据已有数据创建新集合;
|
||||||
|
- 列表推导式使用 `[]`;
|
||||||
|
- 字典推导式使用 `{键: 值}`;
|
||||||
|
- 集合推导式使用 `{值}`;
|
||||||
|
- 末尾的 `if` 用于筛选;
|
||||||
|
- 条件表达式用于从两个结果中选择一个;
|
||||||
|
- 推导式适合简单逻辑,复杂逻辑应使用普通循环;
|
||||||
|
- 清晰比简短更重要。
|
||||||
|
|
||||||
|
## 十九、验收标准
|
||||||
|
|
||||||
|
- 能用普通循环和列表推导式实现相同功能;
|
||||||
|
- 能解释列表推导式各部分的执行顺序;
|
||||||
|
- 能使用末尾 `if` 筛选数据;
|
||||||
|
- 能创建名称到模型的字典;
|
||||||
|
- 能使用集合推导式去重;
|
||||||
|
- 能正确使用简单条件表达式;
|
||||||
|
- 能区分筛选条件与条件表达式;
|
||||||
|
- 所有练习函数通过 `return` 返回结果;
|
||||||
|
- 推导式不会修改原始 Agent 列表;
|
||||||
|
- 没有为了简短而编写难以阅读的复杂表达式。
|
||||||
77
02_python进阶/2_4_推导式与简化写法/comprehension_example.py
Normal file
77
02_python进阶/2_4_推导式与简化写法/comprehension_example.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# 第 2-4 课完整示例:推导式与简化写法
|
||||||
|
#
|
||||||
|
# 本示例先展示普通循环,再展示作用相同的推导式。
|
||||||
|
# 推导式适合简单的数据转换和筛选,不应为了缩短代码而堆叠复杂逻辑。
|
||||||
|
|
||||||
|
|
||||||
|
def get_enabled_agent_names(agents):
|
||||||
|
"""使用列表推导式返回所有已启用 Agent 的名称。"""
|
||||||
|
return [agent["name"] for agent in agents if agent["enabled"]]
|
||||||
|
|
||||||
|
|
||||||
|
def get_agent_tool_counts(agents):
|
||||||
|
"""使用字典推导式建立 Agent 名称与工具数量的对应关系。"""
|
||||||
|
return {
|
||||||
|
agent["name"]: len(agent.get("tools", []))
|
||||||
|
for agent in agents
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_unique_tools(agents):
|
||||||
|
"""使用集合推导式收集不重复的工具名称。"""
|
||||||
|
return {
|
||||||
|
tool
|
||||||
|
for agent in agents
|
||||||
|
for tool in agent.get("tools", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_status_text(agent):
|
||||||
|
"""使用条件表达式返回简短的中文状态。"""
|
||||||
|
return "启用" if agent["enabled"] else "停用"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""准备 Agent 数据,并演示四种简化写法。"""
|
||||||
|
agents = [
|
||||||
|
{
|
||||||
|
"name": "代码助手",
|
||||||
|
"enabled": True,
|
||||||
|
"tools": ["搜索", "终端"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "聊天助手",
|
||||||
|
"enabled": False,
|
||||||
|
"tools": ["搜索"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "测试助手",
|
||||||
|
"enabled": True,
|
||||||
|
"tools": ["终端", "测试"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# 普通循环写法:先创建空列表,再逐条判断和添加。
|
||||||
|
names_from_loop = []
|
||||||
|
for agent in agents:
|
||||||
|
if agent["enabled"]:
|
||||||
|
names_from_loop.append(agent["name"])
|
||||||
|
|
||||||
|
# 列表推导式可以简洁表达同一个“遍历、筛选、保存”过程。
|
||||||
|
enabled_names = get_enabled_agent_names(agents)
|
||||||
|
tool_counts = get_agent_tool_counts(agents)
|
||||||
|
unique_tools = get_unique_tools(agents)
|
||||||
|
|
||||||
|
print(f"普通循环结果:{names_from_loop}")
|
||||||
|
print(f"列表推导式结果:{enabled_names}")
|
||||||
|
print(f"工具数量字典:{tool_counts}")
|
||||||
|
print(f"不重复工具:{sorted(unique_tools)}")
|
||||||
|
print("Agent 状态:")
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
status_text = get_status_text(agent)
|
||||||
|
print(f"- {agent['name']}:{status_text}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
65
02_python进阶/2_4_推导式与简化写法/practice.py
Normal file
65
02_python进阶/2_4_推导式与简化写法/practice.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 第 2-4 课课堂练习:推导式与简化写法
|
||||||
|
#
|
||||||
|
# 请先阅读讲义并运行完整示例,再按照题目顺序完成。
|
||||||
|
# 每个函数都需要使用 return 返回结果,不要只在函数中 print()。
|
||||||
|
# 不要删除题目、测试数据、预期结果和自查注释。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:定义 get_agent_names(agents) 函数
|
||||||
|
# 1. 使用列表推导式遍历 agents。
|
||||||
|
# 2. 取出每个字典中的 name。
|
||||||
|
# 3. return 生成的新列表,不要修改原列表。
|
||||||
|
# 4. 调用后保存结果并输出。
|
||||||
|
# 5. 预期结果:['代码助手', '聊天助手', '测试助手']。
|
||||||
|
|
||||||
|
# 第二部分:定义 get_enabled_agent_names(agents) 函数
|
||||||
|
# 1. 使用带 if 的列表推导式。
|
||||||
|
# 2. 只保留 enabled 为 True 的 Agent 名称。
|
||||||
|
# 3. return 新列表。
|
||||||
|
# 4. 预期结果:['代码助手', '测试助手']。
|
||||||
|
|
||||||
|
# 第三部分:定义 get_model_mapping(agents) 函数
|
||||||
|
# 1. 使用字典推导式。
|
||||||
|
# 2. 字典的键是 Agent 名称,值是模型名称。
|
||||||
|
# 3. return 新字典。
|
||||||
|
# 4. 预期结果:
|
||||||
|
# {'代码助手': 'gpt-5', '聊天助手': 'o3', '测试助手': 'gpt-5'}。
|
||||||
|
|
||||||
|
# 第四部分:定义 get_unique_tools(agents) 函数
|
||||||
|
# 1. 使用集合推导式和两层 for。
|
||||||
|
# 2. 外层遍历 agents,内层遍历每个 agent.get("tools", [])。
|
||||||
|
# 3. return 不重复的工具集合。
|
||||||
|
# 4. 预期集合包含:搜索、终端、测试。
|
||||||
|
# 5. 集合没有固定顺序,验收时比较内容,不比较显示顺序。
|
||||||
|
|
||||||
|
# 第五部分:定义 get_status_text(enabled) 函数
|
||||||
|
# 1. 使用条件表达式,不使用多行 if...else。
|
||||||
|
# 2. enabled 为 True 时 return "启用",否则 return "停用"。
|
||||||
|
# 3. 分别传入 True 和 False。
|
||||||
|
# 4. 预期结果分别是“启用”和“停用”。
|
||||||
|
|
||||||
|
# 第六部分:定义 main() 函数
|
||||||
|
# 1. 依次调用前面五个函数,并把每个返回值保存到有意义的变量中。
|
||||||
|
# 2. 使用 print() 输出所有 Agent 名称、启用名称、模型字典和工具集合。
|
||||||
|
# 3. 调用 get_status_text(agents[0]["enabled"]) 并输出第一个 Agent 的状态。
|
||||||
|
# 4. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||||
|
|
||||||
|
# 最终验收测试:
|
||||||
|
# 1. get_agent_names(agents) 返回三个名称;
|
||||||
|
# 2. get_enabled_agent_names(agents) 只返回两个启用的名称;
|
||||||
|
# 3. get_model_mapping(agents) 返回名称到模型的正确映射;
|
||||||
|
# 4. get_unique_tools(agents) 返回三个不重复工具;
|
||||||
|
# 5. get_status_text(True) 返回“启用”;
|
||||||
|
# 6. get_status_text(False) 返回“停用”;
|
||||||
|
# 7. 所有函数都使用 return 返回结果;
|
||||||
|
# 8. 推导式没有修改原始 agents;
|
||||||
|
# 9. 没有使用嵌套条件表达式或难以阅读的复杂推导式。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否能从左到右解释每个推导式的执行过程;
|
||||||
|
# 2. 是否分清列表、字典和集合推导式使用的括号;
|
||||||
|
# 3. 是否知道筛选条件写在推导式末尾;
|
||||||
|
# 4. 是否知道条件表达式的结果分别写在 if 两侧;
|
||||||
|
# 5. 是否优先保证可读性,而不是一味缩短代码;
|
||||||
|
# 6. 是否保留了完整题目和验收说明。
|
||||||
371
02_python进阶/2_5_迭代器与生成器/README.md
Normal file
371
02_python进阶/2_5_迭代器与生成器/README.md
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
# 第 2-5 课:迭代器与生成器
|
||||||
|
|
||||||
|
## 一、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 解释可迭代对象、迭代器和生成器分别是什么;
|
||||||
|
2. 使用 `iter()` 创建迭代器;
|
||||||
|
3. 使用 `next()` 逐个取得数据;
|
||||||
|
4. 理解数据取完时出现的 `StopIteration`;
|
||||||
|
5. 使用包含 `yield` 的生成器函数逐个产生结果;
|
||||||
|
6. 说明生成器为什么适合处理大量数据;
|
||||||
|
7. 理解同一个生成器被消费后不能自动回到开头。
|
||||||
|
|
||||||
|
## 二、前置知识
|
||||||
|
|
||||||
|
学习本课前,需要掌握:
|
||||||
|
|
||||||
|
- 列表、字典和集合;
|
||||||
|
- `for` 循环和 `while` 循环;
|
||||||
|
- 函数、参数和返回值;
|
||||||
|
- `try...except` 异常处理;
|
||||||
|
- 列表推导式。
|
||||||
|
|
||||||
|
## 三、从 `for` 循环开始理解
|
||||||
|
|
||||||
|
我们已经多次使用 `for` 遍历列表:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_names = ["代码助手", "聊天助手", "测试助手"]
|
||||||
|
|
||||||
|
for agent_name in agent_names:
|
||||||
|
print(agent_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
列表可以依次提供其中的数据,因此它是可迭代对象(Iterable)。可迭代对象可以暂时理解为“能够被 `for` 循环逐项访问的数据”。
|
||||||
|
|
||||||
|
目前学过的常见可迭代对象包括:
|
||||||
|
|
||||||
|
- 字符串;
|
||||||
|
- 列表;
|
||||||
|
- 元组;
|
||||||
|
- 字典;
|
||||||
|
- 集合;
|
||||||
|
- `range()` 产生的对象。
|
||||||
|
|
||||||
|
## 四、迭代器是什么
|
||||||
|
|
||||||
|
迭代器(Iterator)是记录当前遍历位置,并能提供下一个值的对象。
|
||||||
|
|
||||||
|
使用 `iter()` 可以根据可迭代对象创建迭代器:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_names = ["代码助手", "聊天助手"]
|
||||||
|
name_iterator = iter(agent_names)
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 `next()` 逐个取得值:
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(next(name_iterator))
|
||||||
|
print(next(name_iterator))
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:
|
||||||
|
|
||||||
|
```text
|
||||||
|
代码助手
|
||||||
|
聊天助手
|
||||||
|
```
|
||||||
|
|
||||||
|
每调用一次 `next()`,迭代器都会向后移动一步。它不会自动回到开头。
|
||||||
|
|
||||||
|
## 五、`StopIteration`
|
||||||
|
|
||||||
|
当迭代器中的数据全部取完后,再调用 `next()` 会出现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
StopIteration
|
||||||
|
```
|
||||||
|
|
||||||
|
停止迭代异常(Stop Iteration,`StopIteration`)表示迭代器已经没有下一个值。
|
||||||
|
|
||||||
|
可以手动处理:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
print(next(name_iterator))
|
||||||
|
except StopIteration:
|
||||||
|
print("数据已经全部取完。")
|
||||||
|
```
|
||||||
|
|
||||||
|
`for` 循环实际上会在内部不断取得下一个值,并在遇到 `StopIteration` 时自动结束,所以平时使用 `for` 不需要自己捕获它。
|
||||||
|
|
||||||
|
## 六、手动还原 `for` 循环的过程
|
||||||
|
|
||||||
|
下面两段代码表达相似的过程。
|
||||||
|
|
||||||
|
使用 `for`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
for value in values:
|
||||||
|
print(value)
|
||||||
|
```
|
||||||
|
|
||||||
|
手动使用迭代器:
|
||||||
|
|
||||||
|
```python
|
||||||
|
value_iterator = iter(values)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
value = next(value_iterator)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
|
||||||
|
print(value)
|
||||||
|
```
|
||||||
|
|
||||||
|
真实的 `for` 循环由 Python 自动完成这些步骤。本课手动编写一次,是为了理解其工作方式,而不是建议以后都替换 `for`。
|
||||||
|
|
||||||
|
## 七、生成器是什么
|
||||||
|
|
||||||
|
生成器(Generator)是一种特殊的迭代器。它可以按需要逐个产生数据,不必先把全部结果存入列表。
|
||||||
|
|
||||||
|
包含 `yield` 的函数称为生成器函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def generate_numbers():
|
||||||
|
yield 1
|
||||||
|
yield 2
|
||||||
|
yield 3
|
||||||
|
```
|
||||||
|
|
||||||
|
调用普通函数时,函数会立即执行并返回结果。调用生成器函数时,Python 先创建生成器对象:
|
||||||
|
|
||||||
|
```python
|
||||||
|
number_generator = generate_numbers()
|
||||||
|
```
|
||||||
|
|
||||||
|
这时函数体还没有完整执行。可以用 `next()` 或 `for` 逐个取得值:
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(next(number_generator))
|
||||||
|
print(next(number_generator))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 八、`yield` 与 `return` 的区别
|
||||||
|
|
||||||
|
普通函数使用 `return`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_names(agents):
|
||||||
|
names = []
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
names.append(agent["name"])
|
||||||
|
|
||||||
|
return names
|
||||||
|
```
|
||||||
|
|
||||||
|
它会先准备完整列表,然后一次返回。
|
||||||
|
|
||||||
|
生成器函数使用 `yield`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def generate_names(agents):
|
||||||
|
for agent in agents:
|
||||||
|
yield agent["name"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`yield` 会完成三件事:
|
||||||
|
|
||||||
|
1. 交出当前结果;
|
||||||
|
2. 暂停函数;
|
||||||
|
3. 保存当前执行位置,下一次继续从这里向后运行。
|
||||||
|
|
||||||
|
函数中的 `return` 会结束整个函数;`yield` 交出一个值后还能继续生成后面的值。
|
||||||
|
|
||||||
|
## 九、使用 `for` 遍历生成器
|
||||||
|
|
||||||
|
生成器也是迭代器,因此可以直接使用 `for`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
for agent_name in generate_names(agents):
|
||||||
|
print(agent_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
这种写法最常见,也比手动调用 `next()` 更安全。
|
||||||
|
|
||||||
|
如果确实需要把所有结果放入列表,可以使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_names = list(generate_names(agents))
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:这样会立即消费生成器,并把全部结果保存到内存中。
|
||||||
|
|
||||||
|
## 十、为什么需要生成器
|
||||||
|
|
||||||
|
假设需要处理一百万条记录。列表写法通常先准备一百万个结果,再开始使用;生成器可以产生一条、处理一条。
|
||||||
|
|
||||||
|
这种按需产生数据的方式称为惰性求值(Lazy Evaluation)。这里的“惰性”不是运行缓慢,而是“需要时才计算”。
|
||||||
|
|
||||||
|
生成器的主要优点:
|
||||||
|
|
||||||
|
- 不必一次保存全部结果;
|
||||||
|
- 可以更早开始处理第一条数据;
|
||||||
|
- 适合文件逐行读取、大量查询结果或持续数据流。
|
||||||
|
|
||||||
|
本课使用少量 Agent 数据是为了便于观察。数据量很小时,普通列表通常同样合适。
|
||||||
|
|
||||||
|
## 十一、生成器只能继续向后执行
|
||||||
|
|
||||||
|
```python
|
||||||
|
name_generator = generate_names(agents)
|
||||||
|
|
||||||
|
first_result = list(name_generator)
|
||||||
|
second_result = list(name_generator)
|
||||||
|
```
|
||||||
|
|
||||||
|
第一次已经把生成器消费完,因此第二次得到空列表。
|
||||||
|
|
||||||
|
如果确实需要重新遍历,必须重新调用生成器函数创建一个新生成器:
|
||||||
|
|
||||||
|
```python
|
||||||
|
new_generator = generate_names(agents)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十二、生成器表达式
|
||||||
|
|
||||||
|
上一课学习了列表推导式:
|
||||||
|
|
||||||
|
```python
|
||||||
|
enabled_names = [
|
||||||
|
agent["name"]
|
||||||
|
for agent in agents
|
||||||
|
if agent["enabled"]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
把最外层方括号改成圆括号,会得到生成器表达式(Generator Expression):
|
||||||
|
|
||||||
|
```python
|
||||||
|
enabled_name_generator = (
|
||||||
|
agent["name"]
|
||||||
|
for agent in agents
|
||||||
|
if agent["enabled"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
列表推导式立即创建完整列表,生成器表达式按需产生结果。本课练习以 `yield` 为主,先把生成器函数的执行过程理解清楚。
|
||||||
|
|
||||||
|
## 十三、完整示例
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
02_python进阶/2_5_迭代器与生成器/iterator_generator_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例依次演示:
|
||||||
|
|
||||||
|
1. 使用 `iter()` 和 `next()` 手动取出名称;
|
||||||
|
2. 捕获数据取完后的 `StopIteration`;
|
||||||
|
3. 使用生成器筛选启用 Agent;
|
||||||
|
4. 使用生成器逐条产生 Agent 报告。
|
||||||
|
|
||||||
|
## 十四、运行方法
|
||||||
|
|
||||||
|
在项目根目录运行示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_5_迭代器与生成器\iterator_generator_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成练习后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_5_迭代器与生成器\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十五、预期结果
|
||||||
|
|
||||||
|
```text
|
||||||
|
一、手动使用迭代器
|
||||||
|
代码助手
|
||||||
|
聊天助手
|
||||||
|
测试助手
|
||||||
|
名称已经全部取完。
|
||||||
|
==============================
|
||||||
|
二、使用生成器函数
|
||||||
|
生成器对象:generator
|
||||||
|
启用 Agent:代码助手
|
||||||
|
启用 Agent:测试助手
|
||||||
|
==============================
|
||||||
|
三、逐条生成 Agent 报告
|
||||||
|
代码助手|状态:启用|工具数量:2
|
||||||
|
聊天助手|状态:停用|工具数量:1
|
||||||
|
测试助手|状态:启用|工具数量:2
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十六、常见错误
|
||||||
|
|
||||||
|
### 16.1 对可迭代对象直接使用 `next()`
|
||||||
|
|
||||||
|
列表是可迭代对象,但列表本身不是迭代器。应先调用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
name_iterator = iter(agent_names)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 16.2 数据取完后继续调用 `next()`
|
||||||
|
|
||||||
|
迭代器不会自动回到开头。数据取完后继续调用会产生 `StopIteration`。
|
||||||
|
|
||||||
|
### 16.3 在生成器函数中使用 `return result_list`
|
||||||
|
|
||||||
|
如果先创建完整列表再返回,就没有实现按需生成。需要逐个结果时应使用 `yield`。
|
||||||
|
|
||||||
|
### 16.4 以为调用生成器函数就会立即执行
|
||||||
|
|
||||||
|
调用生成器函数只会创建生成器对象。第一次调用 `next()` 或开始 `for` 遍历时,函数才真正向前执行。
|
||||||
|
|
||||||
|
### 16.5 重复消费同一个生成器
|
||||||
|
|
||||||
|
同一个生成器不会自动重置。第二次遍历已经消费完的生成器不会再次得到原数据。
|
||||||
|
|
||||||
|
### 16.6 所有地方都改用生成器
|
||||||
|
|
||||||
|
少量数据需要反复读取时,列表可能更直观。生成器适合按顺序处理、无需返回头部或数据量较大的场景。
|
||||||
|
|
||||||
|
## 十七、课堂练习
|
||||||
|
|
||||||
|
打开 `practice.py`,依次完成:
|
||||||
|
|
||||||
|
1. 使用 `iter()` 和 `next()` 取得前两个名称;
|
||||||
|
2. 手动捕获 `StopIteration` 并消费迭代器;
|
||||||
|
3. 使用 `yield` 生成启用 Agent 名称;
|
||||||
|
4. 使用 `yield` 生成所有工具名称;
|
||||||
|
5. 观察同一个生成器被消费两次的结果;
|
||||||
|
6. 在 `main()` 中输出并核对全部结果。
|
||||||
|
|
||||||
|
## 十八、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。完成后,我会从正确性、可读性和知识掌握情况三个方面验证,并检查生成器函数是否真正使用 `yield` 按需产生数据。
|
||||||
|
|
||||||
|
## 十九、本课小结
|
||||||
|
|
||||||
|
- 可迭代对象能被 `for` 逐项访问;
|
||||||
|
- 迭代器记录当前遍历位置;
|
||||||
|
- `iter()` 创建迭代器,`next()` 取得下一个值;
|
||||||
|
- 数据取完时会出现 `StopIteration`;
|
||||||
|
- `for` 循环会自动处理停止迭代;
|
||||||
|
- 包含 `yield` 的函数是生成器函数;
|
||||||
|
- `yield` 交出一个结果并暂停当前函数;
|
||||||
|
- 生成器按需产生数据,适合顺序处理大量内容;
|
||||||
|
- 同一个生成器消费完后不能自动重新开始。
|
||||||
|
|
||||||
|
## 二十、验收标准
|
||||||
|
|
||||||
|
- 能解释可迭代对象与迭代器的区别;
|
||||||
|
- 能使用 `iter()` 和 `next()` 手动取得数据;
|
||||||
|
- 能正确处理 `StopIteration`;
|
||||||
|
- 能解释 `for` 如何自动结束迭代;
|
||||||
|
- 能编写包含 `yield` 的生成器函数;
|
||||||
|
- 能使用 `for` 和 `list()` 消费生成器;
|
||||||
|
- 能解释 `yield` 与 `return` 的区别;
|
||||||
|
- 能说明同一个生成器第二次消费为什么为空;
|
||||||
|
- 生成器函数没有提前创建完整结果列表;
|
||||||
|
- 示例与练习均不会修改原始 Agent 数据。
|
||||||
74
02_python进阶/2_5_迭代器与生成器/iterator_generator_example.py
Normal file
74
02_python进阶/2_5_迭代器与生成器/iterator_generator_example.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# 第 2-5 课完整示例:迭代器与生成器
|
||||||
|
#
|
||||||
|
# 本示例使用少量固定数据观察迭代过程。
|
||||||
|
# 重点不是缩短代码,而是理解数据如何被逐个取出和按需生成。
|
||||||
|
|
||||||
|
|
||||||
|
def generate_enabled_agent_names(agents):
|
||||||
|
"""逐个生成处于启用状态的 Agent 名称。"""
|
||||||
|
for agent in agents:
|
||||||
|
if agent["enabled"]:
|
||||||
|
# yield 每次只交出一个结果,并暂停函数当前的执行位置。
|
||||||
|
yield agent["name"]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_agent_reports(agents):
|
||||||
|
"""逐个生成 Agent 的中文报告。"""
|
||||||
|
for agent in agents:
|
||||||
|
status_text = "启用" if agent["enabled"] else "停用"
|
||||||
|
tool_count = len(agent.get("tools", []))
|
||||||
|
yield (
|
||||||
|
f"{agent['name']}|状态:{status_text}|"
|
||||||
|
f"工具数量:{tool_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""演示 iter()、next()、StopIteration 和生成器。"""
|
||||||
|
agents = [
|
||||||
|
{
|
||||||
|
"name": "代码助手",
|
||||||
|
"enabled": True,
|
||||||
|
"tools": ["搜索", "终端"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "聊天助手",
|
||||||
|
"enabled": False,
|
||||||
|
"tools": ["搜索"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "测试助手",
|
||||||
|
"enabled": True,
|
||||||
|
"tools": ["终端", "测试"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
print("一、手动使用迭代器")
|
||||||
|
agent_names = [agent["name"] for agent in agents]
|
||||||
|
name_iterator = iter(agent_names)
|
||||||
|
|
||||||
|
print(next(name_iterator))
|
||||||
|
print(next(name_iterator))
|
||||||
|
print(next(name_iterator))
|
||||||
|
|
||||||
|
try:
|
||||||
|
next(name_iterator)
|
||||||
|
except StopIteration:
|
||||||
|
print("名称已经全部取完。")
|
||||||
|
|
||||||
|
print("=" * 30)
|
||||||
|
print("二、使用生成器函数")
|
||||||
|
enabled_name_generator = generate_enabled_agent_names(agents)
|
||||||
|
print(f"生成器对象:{type(enabled_name_generator).__name__}")
|
||||||
|
|
||||||
|
for agent_name in enabled_name_generator:
|
||||||
|
print(f"启用 Agent:{agent_name}")
|
||||||
|
|
||||||
|
print("=" * 30)
|
||||||
|
print("三、逐条生成 Agent 报告")
|
||||||
|
for report in generate_agent_reports(agents):
|
||||||
|
print(report)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
73
02_python进阶/2_5_迭代器与生成器/practice.py
Normal file
73
02_python进阶/2_5_迭代器与生成器/practice.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 第 2-5 课课堂练习:迭代器与生成器
|
||||||
|
#
|
||||||
|
# 请先阅读讲义并运行完整示例,再按照题目顺序完成。
|
||||||
|
# 需要生成多个结果的函数必须使用 yield,不要提前创建完整结果列表。
|
||||||
|
# 不要删除题目、测试数据、预期结果和自查注释。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:手动使用迭代器
|
||||||
|
# 1. 定义 show_first_two_names(agents) 函数。
|
||||||
|
# 2. 使用列表推导式取得所有 Agent 名称。
|
||||||
|
# 3. 使用 iter() 把名称列表转换成迭代器,并保存为 name_iterator。
|
||||||
|
# 4. 连续调用两次 next(),分别保存第一个和第二个名称。
|
||||||
|
# 5. return 一个包含两个名称的元组。
|
||||||
|
# 6. 调用函数并保存结果,预期返回 ("代码助手", "聊天助手")。
|
||||||
|
|
||||||
|
# 第二部分:处理 StopIteration
|
||||||
|
# 1. 定义 consume_iterator(values) 函数,参数接收列表。
|
||||||
|
# 2. 使用 iter(values) 创建迭代器。
|
||||||
|
# 3. 使用 while True 持续调用 next(),把每个结果加入 result 列表。
|
||||||
|
# 4. 捕获 StopIteration 后使用 break 结束循环。
|
||||||
|
# 5. return result。
|
||||||
|
# 6. 传入 ["搜索", "终端"],预期返回相同内容的新列表。
|
||||||
|
# 7. 传入空列表 [],预期返回 [],程序不能崩溃。
|
||||||
|
|
||||||
|
# 第三部分:定义 generate_enabled_names(agents) 生成器函数
|
||||||
|
# 1. 使用 for 遍历 agents。
|
||||||
|
# 2. 只处理 enabled 为 True 的 Agent。
|
||||||
|
# 3. 使用 yield 逐个生成 Agent 名称,不要创建结果列表。
|
||||||
|
# 4. 调用函数时先把生成器对象保存为 enabled_name_generator。
|
||||||
|
# 5. 使用 list(enabled_name_generator) 收集结果并输出。
|
||||||
|
# 6. 预期结果:['代码助手', '测试助手']。
|
||||||
|
|
||||||
|
# 第四部分:定义 generate_tool_names(agents) 生成器函数
|
||||||
|
# 1. 使用两层 for:外层遍历 agents,内层遍历 agent.get("tools", [])。
|
||||||
|
# 2. 使用 yield 逐个生成工具名称。
|
||||||
|
# 3. 本练习不去重,重复工具也需要保留。
|
||||||
|
# 4. 使用 list() 收集生成结果。
|
||||||
|
# 5. 预期结果:['搜索', '终端', '搜索', '终端', '测试']。
|
||||||
|
|
||||||
|
# 第五部分:观察生成器只能继续向后取值
|
||||||
|
# 1. 调用 generate_enabled_names(agents) 创建一个新生成器。
|
||||||
|
# 2. 第一次使用 list() 收集并保存结果,预期有两个名称。
|
||||||
|
# 3. 对同一个生成器再次使用 list(),预期得到空列表。
|
||||||
|
# 4. 输出两次结果,并用自己的话解释第二次为什么为空。
|
||||||
|
# 注意:不要为了让第二次也有数据而重新创建生成器,本题就是观察同一个生成器。
|
||||||
|
|
||||||
|
# 第六部分:定义 main() 函数
|
||||||
|
# 1. 依次调用前面四个函数,并把返回值或生成结果保存到有意义的变量中。
|
||||||
|
# 2. 输出前两个名称、迭代器消费结果、启用名称和全部工具名称。
|
||||||
|
# 3. 完成第五部分的两次生成器收集实验。
|
||||||
|
# 4. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||||
|
|
||||||
|
|
||||||
|
# 最终验收测试:
|
||||||
|
# 1. show_first_two_names(agents) 返回正确的两个名称;
|
||||||
|
# 2. consume_iterator(["搜索", "终端"]) 返回相同内容的新列表;
|
||||||
|
# 3. consume_iterator([]) 返回空列表且不报错;
|
||||||
|
# 4. generate_enabled_names() 是包含 yield 的生成器函数;
|
||||||
|
# 5. 启用名称生成结果为代码助手和测试助手;
|
||||||
|
# 6. generate_tool_names() 是包含 yield 的生成器函数;
|
||||||
|
# 7. 工具生成结果保留重复的搜索和终端;
|
||||||
|
# 8. 同一个生成器第一次收集有数据,第二次收集为空;
|
||||||
|
# 9. 生成器函数中没有先创建完整结果列表再 return。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否能解释可迭代对象和迭代器的区别;
|
||||||
|
# 2. 是否知道 iter() 创建迭代器、next() 取出下一个值;
|
||||||
|
# 3. 是否知道数据取完后会出现 StopIteration;
|
||||||
|
# 4. 是否理解 for 循环会自动处理 StopIteration;
|
||||||
|
# 5. 是否理解 yield 会交出结果并暂停函数;
|
||||||
|
# 6. 是否知道生成器只能从当前位置继续向后执行;
|
||||||
|
# 7. 是否保留了完整题目和验收说明。
|
||||||
357
02_python进阶/2_6_装饰器/README.md
Normal file
357
02_python进阶/2_6_装饰器/README.md
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
# 第 2-6 课:装饰器
|
||||||
|
|
||||||
|
## 一、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 理解函数可以像其他值一样赋值和传递;
|
||||||
|
2. 解释装饰器用于解决什么问题;
|
||||||
|
3. 编写接收函数并返回新函数的简单装饰器;
|
||||||
|
4. 使用 `@装饰器名称` 增强函数;
|
||||||
|
5. 使用 `*args` 和 `**kwargs` 转发参数;
|
||||||
|
6. 保留被装饰函数的返回值;
|
||||||
|
7. 使用 `functools.wraps` 保留原函数信息。
|
||||||
|
|
||||||
|
## 二、前置知识
|
||||||
|
|
||||||
|
学习本课前,需要掌握:
|
||||||
|
|
||||||
|
- 定义和调用函数;
|
||||||
|
- 参数、返回值和局部变量;
|
||||||
|
- `*args` 与 `**kwargs`;
|
||||||
|
- 条件判断;
|
||||||
|
- 模块导入。
|
||||||
|
|
||||||
|
## 三、为什么需要装饰器
|
||||||
|
|
||||||
|
多个函数可能需要执行相同的额外操作,例如:
|
||||||
|
|
||||||
|
- 调用前记录日志;
|
||||||
|
- 检查用户是否有权限;
|
||||||
|
- 检查 Agent 是否启用;
|
||||||
|
- 统计函数执行时间;
|
||||||
|
- 统一处理函数调用结果。
|
||||||
|
|
||||||
|
如果把相同代码复制到每个函数中,修改时容易遗漏。装饰器(Decorator)可以在不修改原函数主体的情况下,为函数统一增加行为。
|
||||||
|
|
||||||
|
本课使用“记录调用日志”和“检查 Agent 状态”两个简单场景。
|
||||||
|
|
||||||
|
## 四、函数也可以作为值
|
||||||
|
|
||||||
|
定义函数后,函数名不加括号表示函数本身:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def greet_agent(name):
|
||||||
|
return f"你好,{name}。"
|
||||||
|
|
||||||
|
|
||||||
|
greeting_function = greet_agent
|
||||||
|
result = greeting_function("代码助手")
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
这里没有调用 `greet_agent`,而是把函数交给另一个变量。随后可以通过新变量调用同一个函数。
|
||||||
|
|
||||||
|
注意区别:
|
||||||
|
|
||||||
|
```python
|
||||||
|
greeting_function = greet_agent # 保存函数本身
|
||||||
|
greeting_result = greet_agent("代码助手") # 立即调用并保存结果
|
||||||
|
```
|
||||||
|
|
||||||
|
装饰器能够工作,正是因为函数可以作为值传入另一个函数,也可以从另一个函数返回。
|
||||||
|
|
||||||
|
## 五、函数内部可以定义函数
|
||||||
|
|
||||||
|
Python 允许在一个函数中定义另一个函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def outer():
|
||||||
|
def inner():
|
||||||
|
return "内部函数的结果"
|
||||||
|
|
||||||
|
return inner
|
||||||
|
```
|
||||||
|
|
||||||
|
`outer()` 返回的是 `inner` 函数本身,而不是它的调用结果:
|
||||||
|
|
||||||
|
```python
|
||||||
|
inner_function = outer()
|
||||||
|
result = inner_function()
|
||||||
|
```
|
||||||
|
|
||||||
|
装饰器通常会在内部定义一个名为 `wrapper` 的函数。`wrapper` 的中文含义是“包装器”,它会包住原函数的调用过程。
|
||||||
|
|
||||||
|
## 六、第一个装饰器
|
||||||
|
|
||||||
|
```python
|
||||||
|
def log_call(func):
|
||||||
|
def wrapper():
|
||||||
|
print("函数开始执行。")
|
||||||
|
result = func()
|
||||||
|
print("函数执行结束。")
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
逐层理解:
|
||||||
|
|
||||||
|
1. `log_call(func)` 接收需要增强的原函数;
|
||||||
|
2. 内部定义 `wrapper()`;
|
||||||
|
3. `wrapper()` 先执行额外逻辑;
|
||||||
|
4. `func()` 调用原函数;
|
||||||
|
5. 保存并返回原函数结果;
|
||||||
|
6. `log_call()` 最后返回 `wrapper` 函数本身。
|
||||||
|
|
||||||
|
这里必须写:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
不要写成 `return wrapper()`,后者会立即调用包装函数。
|
||||||
|
|
||||||
|
## 七、手动使用装饰器
|
||||||
|
|
||||||
|
假设有一个函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_agent_count():
|
||||||
|
return 3
|
||||||
|
```
|
||||||
|
|
||||||
|
可以手动包装:
|
||||||
|
|
||||||
|
```python
|
||||||
|
get_agent_count = log_call(get_agent_count)
|
||||||
|
```
|
||||||
|
|
||||||
|
等号右边把原函数传给装饰器,装饰器返回 `wrapper`;等号左边再让原名称指向包装后的函数。
|
||||||
|
|
||||||
|
以后调用 `get_agent_count()`,实际先进入 `wrapper()`,再由它调用原函数。
|
||||||
|
|
||||||
|
## 八、使用 `@` 语法
|
||||||
|
|
||||||
|
Python 提供了更清楚的写法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@log_call
|
||||||
|
def get_agent_count():
|
||||||
|
return 3
|
||||||
|
```
|
||||||
|
|
||||||
|
它与下面的手动写法表达相同含义:
|
||||||
|
|
||||||
|
```python
|
||||||
|
get_agent_count = log_call(get_agent_count)
|
||||||
|
```
|
||||||
|
|
||||||
|
`@log_call` 必须紧挨着函数定义的上一行。
|
||||||
|
|
||||||
|
## 九、转发不同参数
|
||||||
|
|
||||||
|
如果 `wrapper()` 不接收参数,被装饰函数也就无法正常接收参数。为了适应不同函数,可以使用之前学过的 `*args` 和 `**kwargs`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def log_call(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
print(f"调用函数:{func.__name__}")
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
- `*args` 收集并转发位置参数;
|
||||||
|
- `**kwargs` 收集并转发关键字参数;
|
||||||
|
- `func.__name__` 是原函数名称。
|
||||||
|
|
||||||
|
这使装饰器可以包装参数数量不同的函数。
|
||||||
|
|
||||||
|
## 十、不要丢失原函数返回值
|
||||||
|
|
||||||
|
错误写法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
func(*args, **kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
原函数虽然被调用,但结果没有返回,调用者最终得到 `None`。
|
||||||
|
|
||||||
|
正确过程:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
装饰器可以增加行为,但不应无意中改变原函数正常的返回结果。
|
||||||
|
|
||||||
|
## 十一、使用 `functools.wraps`
|
||||||
|
|
||||||
|
包装后,函数名称默认可能变成 `wrapper`。这会影响调试、日志和帮助信息。
|
||||||
|
|
||||||
|
Python 标准库的 `functools` 模块提供 `wraps`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def log_call(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
`@wraps(func)` 会帮助包装函数保留原函数的名称和说明文字。它本身也是装饰器;本课先掌握固定写法。
|
||||||
|
|
||||||
|
## 十二、调用前进行条件检查
|
||||||
|
|
||||||
|
装饰器可以决定是否调用原函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def require_enabled(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(agent, *args, **kwargs):
|
||||||
|
if not agent.get("enabled", False):
|
||||||
|
print("Agent 已停用,不能执行任务。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return func(agent, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 Agent 已停用,`wrapper` 提前返回,原函数不会执行;如果已启用,参数会继续转交给原函数。
|
||||||
|
|
||||||
|
## 十三、完整示例
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
02_python进阶/2_6_装饰器/decorator_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例演示:
|
||||||
|
|
||||||
|
1. 使用 `@log_operation` 统一增加开始和结束日志;
|
||||||
|
2. 正确转发 Agent 参数;
|
||||||
|
3. 保留工具数量返回值;
|
||||||
|
4. 使用 `@require_enabled` 阻止停用 Agent 执行任务;
|
||||||
|
5. 使用 `@wraps` 保留原函数名称。
|
||||||
|
|
||||||
|
## 十四、运行方法
|
||||||
|
|
||||||
|
在项目根目录运行示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_6_装饰器\decorator_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成练习后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_6_装饰器\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十五、预期结果
|
||||||
|
|
||||||
|
```text
|
||||||
|
一、统一记录操作日志
|
||||||
|
开始执行:count_tools
|
||||||
|
执行结束:count_tools
|
||||||
|
工具数量:2
|
||||||
|
==============================
|
||||||
|
二、执行前检查 Agent 状态
|
||||||
|
聊天助手 当前已停用,无法执行操作。
|
||||||
|
启用 Agent 的结果:代码助手 正在执行任务:检查代码
|
||||||
|
停用 Agent 的结果:None
|
||||||
|
==============================
|
||||||
|
三、检查函数名称
|
||||||
|
被装饰后的函数名:count_tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十六、常见错误
|
||||||
|
|
||||||
|
### 16.1 把函数赋值写成函数调用
|
||||||
|
|
||||||
|
```python
|
||||||
|
saved_function = greet_agent # 保存函数
|
||||||
|
saved_result = greet_agent("代码助手") # 保存结果
|
||||||
|
```
|
||||||
|
|
||||||
|
括号会立即调用函数,两种写法含义不同。
|
||||||
|
|
||||||
|
### 16.2 返回 `wrapper()`
|
||||||
|
|
||||||
|
装饰器最后应返回函数本身:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
写成 `return wrapper()` 会在装饰阶段提前调用。
|
||||||
|
|
||||||
|
### 16.3 忘记调用原函数
|
||||||
|
|
||||||
|
如果 `wrapper` 只输出日志却没有 `func(...)`,原业务功能就不会执行。
|
||||||
|
|
||||||
|
### 16.4 忘记返回原函数结果
|
||||||
|
|
||||||
|
需要使用 `result = func(...)` 和 `return result`,否则调用者可能得到 `None`。
|
||||||
|
|
||||||
|
### 16.5 参数没有继续传递
|
||||||
|
|
||||||
|
包装函数接收到 `*args` 和 `**kwargs` 后,还必须通过 `func(*args, **kwargs)` 转交。
|
||||||
|
|
||||||
|
### 16.6 忘记使用 `@wraps`
|
||||||
|
|
||||||
|
程序可能仍能运行,但函数名称会变成 `wrapper`,不利于日志和调试。
|
||||||
|
|
||||||
|
### 16.7 把所有逻辑都写进装饰器
|
||||||
|
|
||||||
|
装饰器适合通用的附加行为。具体业务逻辑仍应留在原函数中,保持职责清楚。
|
||||||
|
|
||||||
|
## 十七、课堂练习
|
||||||
|
|
||||||
|
打开 `practice.py`,依次完成:
|
||||||
|
|
||||||
|
1. 把函数本身赋值给另一个变量;
|
||||||
|
2. 编写并使用日志装饰器;
|
||||||
|
3. 检查 `@wraps` 是否保留函数名称;
|
||||||
|
4. 编写 Agent 启用状态装饰器;
|
||||||
|
5. 验证停用 Agent 不会执行原函数;
|
||||||
|
6. 在 `main()` 中输出正常和拦截结果。
|
||||||
|
|
||||||
|
## 十八、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。完成后,我会检查装饰器是否真正调用原函数、是否保留参数和返回值,以及停用分支是否正确阻止原函数执行。
|
||||||
|
|
||||||
|
## 十九、本课小结
|
||||||
|
|
||||||
|
- 函数可以赋值给变量,也可以传入和返回;
|
||||||
|
- 装饰器接收原函数并返回包装函数;
|
||||||
|
- `@decorator` 是手动重新赋值的简化语法;
|
||||||
|
- `wrapper` 在调用原函数前后增加统一行为;
|
||||||
|
- `*args` 和 `**kwargs` 可以转发不同参数;
|
||||||
|
- 包装函数应保留原函数返回值;
|
||||||
|
- `@wraps(func)` 可以保留原函数名称等信息;
|
||||||
|
- 装饰器可以在调用前检查条件并阻止无效操作。
|
||||||
|
|
||||||
|
## 二十、验收标准
|
||||||
|
|
||||||
|
- 能解释函数名加括号与不加括号的区别;
|
||||||
|
- 能说明装饰器接收和返回的内容;
|
||||||
|
- 能手动完成一次函数包装;
|
||||||
|
- 能使用 `@` 语法应用装饰器;
|
||||||
|
- `wrapper` 能转发位置参数和关键字参数;
|
||||||
|
- 原函数返回值不会丢失;
|
||||||
|
- `@wraps` 能保留原函数名称;
|
||||||
|
- 启用 Agent 可以执行任务;
|
||||||
|
- 停用 Agent 会被阻止并返回 `None`;
|
||||||
|
- 装饰器只负责通用附加行为,原函数保留具体业务逻辑。
|
||||||
79
02_python进阶/2_6_装饰器/decorator_example.py
Normal file
79
02_python进阶/2_6_装饰器/decorator_example.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# 第 2-6 课完整示例:装饰器
|
||||||
|
#
|
||||||
|
# 装饰器可以在不修改原函数主体的情况下,统一增加额外行为。
|
||||||
|
# 本示例为 Agent 操作增加开始、结束日志,并保留参数和返回值。
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def log_operation(func):
|
||||||
|
"""为函数增加开始和结束日志。"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# func.__name__ 表示被装饰函数原来的名称。
|
||||||
|
print(f"开始执行:{func.__name__}")
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
print(f"执行结束:{func.__name__}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def require_enabled(func):
|
||||||
|
"""只允许对处于启用状态的 Agent 执行操作。"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(agent, *args, **kwargs):
|
||||||
|
if not agent.get("enabled", False):
|
||||||
|
print(f"{agent['name']} 当前已停用,无法执行操作。")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return func(agent, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@log_operation
|
||||||
|
def count_tools(agent):
|
||||||
|
"""返回 Agent 的工具数量。"""
|
||||||
|
return len(agent.get("tools", []))
|
||||||
|
|
||||||
|
|
||||||
|
@require_enabled
|
||||||
|
def run_agent(agent, task_name):
|
||||||
|
"""返回 Agent 执行任务时的说明文字。"""
|
||||||
|
return f"{agent['name']} 正在执行任务:{task_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""演示装饰器如何增强函数并保留返回值。"""
|
||||||
|
code_agent = {
|
||||||
|
"name": "代码助手",
|
||||||
|
"enabled": True,
|
||||||
|
"tools": ["搜索", "终端"],
|
||||||
|
}
|
||||||
|
chat_agent = {
|
||||||
|
"name": "聊天助手",
|
||||||
|
"enabled": False,
|
||||||
|
"tools": ["搜索"],
|
||||||
|
}
|
||||||
|
|
||||||
|
print("一、统一记录操作日志")
|
||||||
|
tool_count = count_tools(code_agent)
|
||||||
|
print(f"工具数量:{tool_count}")
|
||||||
|
|
||||||
|
print("=" * 30)
|
||||||
|
print("二、执行前检查 Agent 状态")
|
||||||
|
enabled_result = run_agent(code_agent, "检查代码")
|
||||||
|
disabled_result = run_agent(chat_agent, "整理对话")
|
||||||
|
print(f"启用 Agent 的结果:{enabled_result}")
|
||||||
|
print(f"停用 Agent 的结果:{disabled_result}")
|
||||||
|
|
||||||
|
print("=" * 30)
|
||||||
|
print("三、检查函数名称")
|
||||||
|
print(f"被装饰后的函数名:{count_tools.__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
83
02_python进阶/2_6_装饰器/practice.py
Normal file
83
02_python进阶/2_6_装饰器/practice.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 第 2-6 课课堂练习:装饰器
|
||||||
|
#
|
||||||
|
# 请先阅读讲义并运行完整示例,再按照题目顺序完成。
|
||||||
|
# 装饰器必须调用原函数并保留原函数的返回值。
|
||||||
|
# 不要删除题目、测试数据、预期结果和自查注释。
|
||||||
|
|
||||||
|
# 第一部分:理解函数可以赋值给变量
|
||||||
|
# 1. 定义 greet_agent(name) 函数。
|
||||||
|
# 2. 函数 return f"你好,{name}。",不要在函数内部 print()。
|
||||||
|
# 3. 把 greet_agent 函数本身赋值给变量 greeting_function,赋值时不要写括号。
|
||||||
|
# 4. 调用 greeting_function("代码助手"),保存并输出返回值。
|
||||||
|
# 5. 预期结果:“你好,代码助手。”。
|
||||||
|
|
||||||
|
# greeting_function = greet_agent
|
||||||
|
# print(greeting_function("代码助手"))
|
||||||
|
|
||||||
|
# 第二部分:定义 log_call(func) 装饰器
|
||||||
|
# 1. 在 log_call() 内定义 wrapper(*args, **kwargs)。
|
||||||
|
# 2. 在 wrapper 上方使用 @wraps(func)。
|
||||||
|
# 3. wrapper 先输出 f"调用函数:{func.__name__}"。
|
||||||
|
# 4. 使用 func(*args, **kwargs) 调用原函数,并把返回值保存为 result。
|
||||||
|
# 5. wrapper return result,不能丢失原函数返回值。
|
||||||
|
# 6. log_call() return wrapper,注意不要写成 wrapper()。
|
||||||
|
|
||||||
|
# 第三部分:使用 @log_call
|
||||||
|
# 1. 定义 get_agent_name(agent) 函数。
|
||||||
|
# 2. 在函数上方写 @log_call。
|
||||||
|
# 3. 函数 return agent["name"]。
|
||||||
|
# 4. 调用 get_agent_name(enabled_agent),保存并输出结果。
|
||||||
|
# 5. 预期先输出“调用函数:get_agent_name”,再得到“代码助手”。
|
||||||
|
# 6. 检查 get_agent_name.__name__,预期仍是 "get_agent_name"。
|
||||||
|
|
||||||
|
# print(get_agent_name(enabled_agent))
|
||||||
|
# print(get_agent_name.__name__)
|
||||||
|
|
||||||
|
# 第四部分:定义 require_enabled(func) 装饰器
|
||||||
|
# 1. 在 require_enabled() 内定义 wrapper(agent, *args, **kwargs)。
|
||||||
|
# 2. 在 wrapper 上方使用 @wraps(func)。
|
||||||
|
# 3. 如果 agent.get("enabled", False) 为 False:
|
||||||
|
# 输出“Agent 已停用,不能执行任务。”并 return None。
|
||||||
|
# 4. 如果 Agent 已启用,调用 func(agent, *args, **kwargs) 并 return 原结果。
|
||||||
|
# 5. require_enabled() return wrapper。
|
||||||
|
|
||||||
|
# 第五部分:使用 @require_enabled
|
||||||
|
# 1. 定义 run_task(agent, task_name) 函数。
|
||||||
|
# 2. 在函数上方写 @require_enabled。
|
||||||
|
# 3. 函数 return f"{agent['name']} 正在执行:{task_name}"。
|
||||||
|
# 4. 传入 enabled_agent 和 "检查代码",预期返回:
|
||||||
|
# “代码助手 正在执行:检查代码”。
|
||||||
|
# 5. 传入 disabled_agent 和 "整理对话",预期输出停用提示并返回 None。
|
||||||
|
|
||||||
|
# print(run_task(enabled_agent ,"检查代码"))
|
||||||
|
# print(run_task(disabled_agent ,"整理对话"))
|
||||||
|
|
||||||
|
# 第六部分:定义 main() 函数
|
||||||
|
# 1. 完成第一部分的函数赋值实验,并输出问候结果。
|
||||||
|
# 2. 调用 get_agent_name(enabled_agent),保存并输出返回值。
|
||||||
|
# 3. 输出 get_agent_name.__name__,确认 @wraps 保留了原函数名。
|
||||||
|
# 4. 分别使用启用和停用 Agent 调用 run_task()。
|
||||||
|
# 5. 保存并输出两个调用结果,停用 Agent 的结果应为 None。
|
||||||
|
# 6. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||||
|
|
||||||
|
# 最终验收测试:
|
||||||
|
# 1. greeting_function 与 greet_agent 指向同一个函数;
|
||||||
|
# 2. greeting_function("代码助手") 返回正确文字;
|
||||||
|
# 3. get_agent_name() 调用前输出函数名称;
|
||||||
|
# 4. get_agent_name(enabled_agent) 返回“代码助手”;
|
||||||
|
# 5. get_agent_name.__name__ 仍是 "get_agent_name";
|
||||||
|
# 6. run_task(enabled_agent, "检查代码") 返回正确文字;
|
||||||
|
# 7. run_task(disabled_agent, "整理对话") 返回 None;
|
||||||
|
# 8. 停用 Agent 不会执行原 run_task() 的函数主体;
|
||||||
|
# 9. 两个装饰器都能接收参数并保留原函数返回值。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否理解函数名不加括号表示函数本身;
|
||||||
|
# 2. 是否理解函数名加括号表示立即调用函数;
|
||||||
|
# 3. 装饰器是否接收原函数并返回 wrapper;
|
||||||
|
# 4. wrapper 是否调用并返回原函数结果;
|
||||||
|
# 5. 是否使用 *args 和 **kwargs 转发参数;
|
||||||
|
# 6. 是否使用 @wraps(func) 保留原函数信息;
|
||||||
|
# 7. 是否能解释 @log_call 与手动重新赋值的关系;
|
||||||
|
# 8. 是否保留了完整题目和验收说明。
|
||||||
383
02_python进阶/2_7_类型注解/README.md
Normal file
383
02_python进阶/2_7_类型注解/README.md
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
# 第 2-7 课:类型注解
|
||||||
|
|
||||||
|
## 一、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 解释类型注解是什么,以及它能解决什么问题;
|
||||||
|
2. 为变量、函数参数和返回值添加类型注解;
|
||||||
|
3. 标注 `str`、`int`、`float` 和 `bool`;
|
||||||
|
4. 标注字符串列表与字符串字典;
|
||||||
|
5. 使用 `类型 | None` 表示结果可能不存在;
|
||||||
|
6. 使用 `-> None` 标注只执行操作、不返回业务结果的函数;
|
||||||
|
7. 理解类型注解通常不会在运行时自动强制检查数据。
|
||||||
|
|
||||||
|
## 二、前置知识
|
||||||
|
|
||||||
|
学习本课前,需要掌握:
|
||||||
|
|
||||||
|
- 变量和基本数据类型;
|
||||||
|
- 列表与字典;
|
||||||
|
- 函数、参数与返回值;
|
||||||
|
- 条件判断;
|
||||||
|
- `None` 的基本含义。
|
||||||
|
|
||||||
|
## 三、为什么需要类型注解
|
||||||
|
|
||||||
|
观察下面的函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def format_status(name, enabled):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
只看函数定义,我们无法立即确定:
|
||||||
|
|
||||||
|
- `name` 应该传字符串还是字典;
|
||||||
|
- `enabled` 应该传布尔值还是文字;
|
||||||
|
- 函数最后返回什么类型。
|
||||||
|
|
||||||
|
添加类型注解(Type Hint):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def format_status(name: str, enabled: bool) -> str:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
现在可以看出:
|
||||||
|
|
||||||
|
- `name` 预期是字符串;
|
||||||
|
- `enabled` 预期是布尔值;
|
||||||
|
- `-> str` 表示预期返回字符串。
|
||||||
|
|
||||||
|
类型注解的主要作用是帮助人和工具理解代码。
|
||||||
|
|
||||||
|
## 四、类型注解不会自动改变数据
|
||||||
|
|
||||||
|
```python
|
||||||
|
age: int = 18
|
||||||
|
```
|
||||||
|
|
||||||
|
`int` 说明变量预期保存整数,但它不会执行类型转换。下面两段代码不同:
|
||||||
|
|
||||||
|
```python
|
||||||
|
age: int = "18" # 只是写了注解,值仍然是字符串
|
||||||
|
age = int("18") # 真正把字符串转换为整数
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 通常也不会因为注解与实际值不一致而立即阻止程序运行。编辑器或静态类型检查工具可以提前提示问题。
|
||||||
|
|
||||||
|
静态类型检查(Static Type Checking)表示在不真正执行全部业务代码的情况下,根据注解检查可能的类型错误。
|
||||||
|
|
||||||
|
## 五、变量类型注解
|
||||||
|
|
||||||
|
基本格式:
|
||||||
|
|
||||||
|
```python
|
||||||
|
变量名: 类型 = 值
|
||||||
|
```
|
||||||
|
|
||||||
|
常见示例:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_name: str = "代码助手"
|
||||||
|
tool_count: int = 2
|
||||||
|
completion_rate: float = 0.75
|
||||||
|
enabled: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
冒号后面是预期类型,等号后面仍然是实际值。
|
||||||
|
|
||||||
|
简单变量的类型通常很容易从值中看出,因此不必强迫每个局部变量都写注解。类型不明显或需要强调时再添加。
|
||||||
|
|
||||||
|
## 六、参数和返回值注解
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_status_text(enabled: bool) -> str:
|
||||||
|
return "启用" if enabled else "停用"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `enabled: bool` 是参数注解;
|
||||||
|
- `-> str` 是返回值注解;
|
||||||
|
- 注解不影响函数正常调用方式。
|
||||||
|
|
||||||
|
调用仍然写成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
status_text = get_status_text(True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 七、常见基本类型
|
||||||
|
|
||||||
|
本课使用以下类型:
|
||||||
|
|
||||||
|
| 注解 | 表示的数据 |
|
||||||
|
|---|---|
|
||||||
|
| `str` | 字符串 |
|
||||||
|
| `int` | 整数 |
|
||||||
|
| `float` | 浮点数 |
|
||||||
|
| `bool` | 布尔值 |
|
||||||
|
| `None` | 没有业务结果 |
|
||||||
|
|
||||||
|
类型名称不需要加引号:
|
||||||
|
|
||||||
|
```python
|
||||||
|
name: str = "代码助手"
|
||||||
|
```
|
||||||
|
|
||||||
|
不要写成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
name: "str" = "代码助手"
|
||||||
|
```
|
||||||
|
|
||||||
|
字符串形式的类型注解有其他用途,本课暂不展开。
|
||||||
|
|
||||||
|
## 八、列表类型注解
|
||||||
|
|
||||||
|
字符串列表:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tools: list[str] = ["搜索", "终端"]
|
||||||
|
```
|
||||||
|
|
||||||
|
可以从外向内理解:
|
||||||
|
|
||||||
|
- `list`:这是列表;
|
||||||
|
- `[str]`:列表中的元素预期是字符串。
|
||||||
|
|
||||||
|
函数示例:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def count_tools(tools: list[str]) -> int:
|
||||||
|
return len(tools)
|
||||||
|
```
|
||||||
|
|
||||||
|
返回字符串列表:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_names() -> list[str]:
|
||||||
|
return ["代码助手", "聊天助手"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 九、字典类型注解
|
||||||
|
|
||||||
|
名称到模型的对应关系,可以标注为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
models: dict[str, str] = {
|
||||||
|
"代码助手": "gpt-5",
|
||||||
|
"聊天助手": "o3",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`dict[str, str]` 表示:
|
||||||
|
|
||||||
|
- 第一个 `str`:字典键是字符串;
|
||||||
|
- 第二个 `str`:字典值也是字符串。
|
||||||
|
|
||||||
|
任务列表可能写成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
tasks: list[dict[str, str]] = [
|
||||||
|
{"title": "学习类型注解", "status": "已完成"},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
从外向内理解:这是一个列表,列表中每项是字典,字典的键和值都是字符串。
|
||||||
|
|
||||||
|
## 十、结果可能是 `None`
|
||||||
|
|
||||||
|
查找操作可能找到字符串,也可能找不到:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def find_model(
|
||||||
|
models: dict[str, str],
|
||||||
|
agent_name: str,
|
||||||
|
) -> str | None:
|
||||||
|
return models.get(agent_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
`str | None` 表示返回值有两种可能:
|
||||||
|
|
||||||
|
- 找到时返回 `str`;
|
||||||
|
- 未找到时返回 `None`。
|
||||||
|
|
||||||
|
符号 `|` 可以理解为“或者”。
|
||||||
|
|
||||||
|
调用者应检查:
|
||||||
|
|
||||||
|
```python
|
||||||
|
model_name = find_model(models, "测试助手")
|
||||||
|
|
||||||
|
if model_name is None:
|
||||||
|
print("没有找到模型。")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十一、返回值标注为 `None`
|
||||||
|
|
||||||
|
只负责输出、不返回业务结果的函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def print_agent(name: str) -> None:
|
||||||
|
print(name)
|
||||||
|
```
|
||||||
|
|
||||||
|
`-> None` 表示调用者不应期待它返回可供后续使用的业务数据。
|
||||||
|
|
||||||
|
程序入口也常写成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def main() -> None:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十二、类型注解和默认值
|
||||||
|
|
||||||
|
默认值写在类型注解之后:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_agent(name: str, enabled: bool = True) -> str:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
阅读顺序:
|
||||||
|
|
||||||
|
- 参数名是 `enabled`;
|
||||||
|
- 类型是 `bool`;
|
||||||
|
- 默认值是 `True`。
|
||||||
|
|
||||||
|
不要把类型和默认值的位置写反。
|
||||||
|
|
||||||
|
## 十三、类型注解能带来什么
|
||||||
|
|
||||||
|
类型注解可以帮助:
|
||||||
|
|
||||||
|
- 阅读者更快理解参数和返回值;
|
||||||
|
- 编辑器提供更准确的自动补全;
|
||||||
|
- 编辑器提前提示可能的类型错误;
|
||||||
|
- 重构代码时发现受影响的位置;
|
||||||
|
- 静态类型检查工具检查大型项目。
|
||||||
|
|
||||||
|
类型注解不能代替:
|
||||||
|
|
||||||
|
- 运行时数据校验;
|
||||||
|
- `try...except` 异常处理;
|
||||||
|
- 业务规则判断;
|
||||||
|
- 单元测试。
|
||||||
|
|
||||||
|
例如,Web 接口收到的外部数据仍然需要真实校验,不能只依靠注解。
|
||||||
|
|
||||||
|
## 十四、完整示例
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
02_python进阶/2_7_类型注解/type_annotation_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例会演示:
|
||||||
|
|
||||||
|
1. 字符串和布尔参数注解;
|
||||||
|
2. 字符串列表和字符串字典;
|
||||||
|
3. 整数返回值;
|
||||||
|
4. `str | None`;
|
||||||
|
5. 只输出函数的 `-> None`。
|
||||||
|
|
||||||
|
## 十五、运行方法
|
||||||
|
|
||||||
|
在项目根目录运行示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_7_类型注解\type_annotation_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成练习后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_7_类型注解\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十六、预期结果
|
||||||
|
|
||||||
|
```text
|
||||||
|
代码助手|状态:启用
|
||||||
|
模型:gpt-5
|
||||||
|
不存在的模型:None
|
||||||
|
Agent 名称:代码助手
|
||||||
|
工具数量:2
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十七、常见错误
|
||||||
|
|
||||||
|
### 17.1 把注解当作类型转换
|
||||||
|
|
||||||
|
```python
|
||||||
|
count: int = "2"
|
||||||
|
```
|
||||||
|
|
||||||
|
实际值仍然是字符串。需要转换时必须使用 `int("2")`。
|
||||||
|
|
||||||
|
### 17.2 忘记返回值箭头
|
||||||
|
|
||||||
|
参数使用冒号,返回值使用 `->`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def count_tools(tools: list[str]) -> int:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 17.3 列表没有写元素类型
|
||||||
|
|
||||||
|
只写 `list` 无法说明元素是什么。本课优先写成 `list[str]`。
|
||||||
|
|
||||||
|
### 17.4 混淆字典键和值的类型
|
||||||
|
|
||||||
|
`dict[str, int]` 表示字符串键和整数值,前后顺序不能颠倒。
|
||||||
|
|
||||||
|
### 17.5 可能返回 `None` 却只标注字符串
|
||||||
|
|
||||||
|
如果函数可能找不到结果,应标注 `str | None`,提醒调用者处理空结果。
|
||||||
|
|
||||||
|
### 17.6 认为有注解就不需要测试
|
||||||
|
|
||||||
|
类型正确不代表业务结果正确。边界条件、异常和实际输出仍需验证。
|
||||||
|
|
||||||
|
## 十八、课堂练习
|
||||||
|
|
||||||
|
打开 `practice.py`,依次完成:
|
||||||
|
|
||||||
|
1. 为任务状态格式化函数添加基本类型;
|
||||||
|
2. 创建并返回字符串字典;
|
||||||
|
3. 处理任务字典列表;
|
||||||
|
4. 使用 `float | None` 表示两种完成率结果;
|
||||||
|
5. 使用 `-> None` 标注输出函数和主函数;
|
||||||
|
6. 运行正常与零任务数用例。
|
||||||
|
|
||||||
|
## 十九、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。完成后,我会验证函数行为与 `__annotations__` 中的实际注解,并检查注解是否与真实返回值一致。
|
||||||
|
|
||||||
|
## 二十、本课小结
|
||||||
|
|
||||||
|
- 类型注解说明代码预期使用的类型;
|
||||||
|
- 参数类型写在冒号后,返回类型写在 `->` 后;
|
||||||
|
- `list[str]` 表示字符串列表;
|
||||||
|
- `dict[str, str]` 表示字符串键和字符串值;
|
||||||
|
- `str | None` 表示可能返回字符串或 `None`;
|
||||||
|
- 只执行操作的函数可以标注 `-> None`;
|
||||||
|
- 类型注解不会自动转换或验证运行时数据;
|
||||||
|
- 类型注解提高可读性,但不能代替校验、异常处理和测试。
|
||||||
|
|
||||||
|
## 二十一、验收标准
|
||||||
|
|
||||||
|
- 能解释类型注解的作用;
|
||||||
|
- 能为基本类型参数和返回值添加注解;
|
||||||
|
- 能正确标注字符串列表;
|
||||||
|
- 能正确标注字符串字典;
|
||||||
|
- 能看懂嵌套的 `list[dict[str, str]]`;
|
||||||
|
- 能使用 `float | None` 表达两种结果;
|
||||||
|
- 能使用 `-> None` 标注输出函数;
|
||||||
|
- 能说明注解与类型转换的区别;
|
||||||
|
- 能说明类型注解通常不会自动强制检查运行时数据;
|
||||||
|
- 所有注解与函数真实返回值保持一致。
|
||||||
84
02_python进阶/2_7_类型注解/practice.py
Normal file
84
02_python进阶/2_7_类型注解/practice.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 第 2-7 课课堂练习:类型注解
|
||||||
|
#
|
||||||
|
# 请先阅读讲义并运行完整示例,再按照题目顺序完成。
|
||||||
|
# 每个函数都需要写参数类型和返回值类型。
|
||||||
|
# 不要删除题目、测试数据、预期结果和自查注释。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:定义 format_task_status(title, completed) 函数
|
||||||
|
# 1. 参数 title 标注为 str。
|
||||||
|
# 2. 参数 completed 标注为 bool。
|
||||||
|
# 3. 返回值标注为 str。
|
||||||
|
# 4. completed 为 True 时状态文字是“已完成”,否则是“未完成”。
|
||||||
|
# 5. return f"{title}|状态:{status_text}"。
|
||||||
|
# 6. 传入“学习类型注解”和 True,预期返回:
|
||||||
|
# “学习类型注解|状态:已完成”。
|
||||||
|
|
||||||
|
# print(format_task_status("学习类型注解", True))
|
||||||
|
|
||||||
|
# 第二部分:定义 build_task(title, status) 函数
|
||||||
|
# 1. 两个参数都标注为 str。
|
||||||
|
# 2. 返回值标注为 dict[str, str]。
|
||||||
|
# 3. return 包含 title 和 status 的新字典。
|
||||||
|
# 4. 传入“复习生成器”和“未开始”,预期返回:
|
||||||
|
# {"title": "复习生成器", "status": "未开始"}。
|
||||||
|
|
||||||
|
# print(build_task("复习生成器", "未开始"))
|
||||||
|
|
||||||
|
# 第三部分:定义 get_task_titles(tasks) 函数
|
||||||
|
# 1. 参数标注为 list[dict[str, str]]。
|
||||||
|
# 2. 返回值标注为 list[str]。
|
||||||
|
# 3. 使用列表推导式取得每个任务的 title。
|
||||||
|
# 4. return 新列表,不要修改原 tasks。
|
||||||
|
# 5. 使用顶部测试数据,预期返回:
|
||||||
|
# ["学习类型注解", "完成课堂练习"]。
|
||||||
|
|
||||||
|
# print(get_task_titles(tasks))
|
||||||
|
|
||||||
|
# 第四部分:定义 calculate_completion_rate(completed_count, total_count) 函数
|
||||||
|
# 1. 两个参数都标注为 int。
|
||||||
|
# 2. 返回值标注为 float | None,表示可能返回浮点数,也可能返回 None。
|
||||||
|
# 3. total_count 为 0 时 return None。
|
||||||
|
# 4. 否则 return completed_count / total_count。
|
||||||
|
# 5. 传入 3 和 4,预期返回 0.75。
|
||||||
|
# 6. 传入 0 和 0,预期返回 None,程序不能崩溃。
|
||||||
|
|
||||||
|
# print(calculate_completion_rate(3,4))
|
||||||
|
# print(calculate_completion_rate(0,0))
|
||||||
|
|
||||||
|
# 第五部分:定义 print_task(task) 函数
|
||||||
|
# 1. 参数标注为 dict[str, str]。
|
||||||
|
# 2. 返回值标注为 None,因为本函数只输出,不返回业务结果。
|
||||||
|
# 3. 按“任务:标题|状态:状态文字”的格式输出。
|
||||||
|
# 4. 传入第一个测试任务,预期输出:
|
||||||
|
# “任务:学习类型注解|状态:已完成”。
|
||||||
|
|
||||||
|
# print_task(tasks[0])
|
||||||
|
|
||||||
|
# 第六部分:定义 main() 函数
|
||||||
|
# 1. 返回值标注为 None。
|
||||||
|
# 2. 依次调用前面五个函数,并保存需要使用的返回值。
|
||||||
|
# 3. 输出格式化状态、新任务字典、任务标题列表和两种完成率。
|
||||||
|
# 4. 调用 print_task(tasks[0]) 输出第一个任务。
|
||||||
|
# 5. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||||
|
|
||||||
|
# 最终验收测试:
|
||||||
|
# 1. format_task_status() 的参数和返回值注解正确;
|
||||||
|
# 2. build_task() 返回正确字典,并标注 dict[str, str];
|
||||||
|
# 3. get_task_titles() 返回正确列表,并标注 list[str];
|
||||||
|
# 4. calculate_completion_rate(3, 4) 返回 0.75;
|
||||||
|
# 5. calculate_completion_rate(0, 0) 返回 None;
|
||||||
|
# 6. 完成率返回类型标注为 float | None;
|
||||||
|
# 7. print_task() 和 main() 的返回值标注为 None;
|
||||||
|
# 8. 原始 tasks 没有被修改;
|
||||||
|
# 9. 直接运行程序时,所有结果均正确输出。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否知道冒号后写参数或变量类型;
|
||||||
|
# 2. 是否知道 -> 后写函数返回值类型;
|
||||||
|
# 3. 是否能区分 list[str] 和 dict[str, str];
|
||||||
|
# 4. 是否理解 float | None 表示两种可能结果;
|
||||||
|
# 5. 是否知道只输出的函数返回值标注为 None;
|
||||||
|
# 6. 是否理解类型注解通常不会自动检查运行时数据;
|
||||||
|
# 7. 是否保留了完整题目和验收说明。
|
||||||
53
02_python进阶/2_7_类型注解/type_annotation_example.py
Normal file
53
02_python进阶/2_7_类型注解/type_annotation_example.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# 第 2-7 课完整示例:类型注解
|
||||||
|
#
|
||||||
|
# 类型注解用于说明变量、参数和返回值预期使用的类型。
|
||||||
|
# Python 通常不会在运行时自动强制检查这些注解。
|
||||||
|
|
||||||
|
|
||||||
|
def format_agent_status(name: str, enabled: bool) -> str:
|
||||||
|
"""根据名称和启用状态返回中文说明。"""
|
||||||
|
status_text = "启用" if enabled else "停用"
|
||||||
|
return f"{name}|状态:{status_text}"
|
||||||
|
|
||||||
|
|
||||||
|
def count_tools(tools: list[str]) -> int:
|
||||||
|
"""返回工具名称列表中的元素数量。"""
|
||||||
|
return len(tools)
|
||||||
|
|
||||||
|
|
||||||
|
def find_agent_model(
|
||||||
|
model_mapping: dict[str, str],
|
||||||
|
agent_name: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""返回指定 Agent 的模型,找不到时返回 None。"""
|
||||||
|
return model_mapping.get(agent_name)
|
||||||
|
|
||||||
|
|
||||||
|
def print_agent_report(name: str, tools: list[str]) -> None:
|
||||||
|
"""输出 Agent 报告,本函数不返回业务结果。"""
|
||||||
|
print(f"Agent 名称:{name}")
|
||||||
|
print(f"工具数量:{count_tools(tools)}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""准备带类型注解的数据并调用示例函数。"""
|
||||||
|
agent_name: str = "代码助手"
|
||||||
|
enabled: bool = True
|
||||||
|
tools: list[str] = ["搜索", "终端"]
|
||||||
|
model_mapping: dict[str, str] = {
|
||||||
|
"代码助手": "gpt-5",
|
||||||
|
"聊天助手": "o3",
|
||||||
|
}
|
||||||
|
|
||||||
|
status_text = format_agent_status(agent_name, enabled)
|
||||||
|
model_name = find_agent_model(model_mapping, agent_name)
|
||||||
|
missing_model = find_agent_model(model_mapping, "测试助手")
|
||||||
|
|
||||||
|
print(status_text)
|
||||||
|
print(f"模型:{model_name}")
|
||||||
|
print(f"不存在的模型:{missing_model}")
|
||||||
|
print_agent_report(agent_name, tools)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
410
02_python进阶/2_8_虚拟环境与依赖管理/README.md
Normal file
410
02_python进阶/2_8_虚拟环境与依赖管理/README.md
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
# 第 2-8 课:虚拟环境与依赖管理
|
||||||
|
|
||||||
|
## 一、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 解释虚拟环境是什么,以及为什么需要它;
|
||||||
|
2. 在 Windows PowerShell 中创建、激活和退出虚拟环境;
|
||||||
|
3. 确认命令实际使用的是哪个 Python 解释器;
|
||||||
|
4. 使用 `python -m pip` 查看、安装和卸载第三方依赖;
|
||||||
|
5. 使用 `requirements.txt` 记录与恢复项目依赖;
|
||||||
|
6. 理解版本固定的基本作用;
|
||||||
|
7. 知道 `.venv` 不应提交到 Git。
|
||||||
|
|
||||||
|
## 二、前置知识
|
||||||
|
|
||||||
|
学习本课前,需要掌握:
|
||||||
|
|
||||||
|
- 在 PowerShell 中进入目录并运行 Python 文件;
|
||||||
|
- 模块与导入;
|
||||||
|
- 文件和路径;
|
||||||
|
- 基础异常处理;
|
||||||
|
- 类型注解。
|
||||||
|
|
||||||
|
## 三、什么是第三方依赖
|
||||||
|
|
||||||
|
Python 自带的模块组成标准库(Standard Library),例如 `pathlib`、`sys` 和 `platform`,使用它们通常不需要安装额外内容。
|
||||||
|
|
||||||
|
第三方依赖(Third-Party Dependency)是其他开发者发布、项目需要额外安装的软件包。例如以后 Web 阶段会使用 FastAPI、Django 等框架。
|
||||||
|
|
||||||
|
安装依赖后,我们可以在代码中导入它:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import package_name
|
||||||
|
```
|
||||||
|
|
||||||
|
这里的 `package_name` 只是格式示例,不要求本课安装这个不存在的软件包。
|
||||||
|
|
||||||
|
## 四、为什么不能把所有依赖都装在同一个环境
|
||||||
|
|
||||||
|
假设有两个项目:
|
||||||
|
|
||||||
|
- 项目 A 需要某个软件包的旧版本;
|
||||||
|
- 项目 B 需要同一个软件包的新版本。
|
||||||
|
|
||||||
|
如果两个项目共用一个 Python 环境,升级项目 B 的依赖可能导致项目 A 无法运行。
|
||||||
|
|
||||||
|
还可能遇到:
|
||||||
|
|
||||||
|
- 不知道某个依赖属于哪个项目;
|
||||||
|
- 删除一个依赖时误伤其他项目;
|
||||||
|
- 自己的机器能运行,换一台机器却缺少依赖;
|
||||||
|
- 项目依赖越来越杂乱。
|
||||||
|
|
||||||
|
## 五、什么是虚拟环境
|
||||||
|
|
||||||
|
虚拟环境(Virtual Environment)是为一个项目准备的独立 Python 运行环境。它拥有自己的 Python 入口和依赖安装位置。
|
||||||
|
|
||||||
|
可以把它理解为每个项目自己的“工具箱”:
|
||||||
|
|
||||||
|
```text
|
||||||
|
项目 A → 自己的虚拟环境 → 自己的依赖
|
||||||
|
项目 B → 自己的虚拟环境 → 自己的依赖
|
||||||
|
```
|
||||||
|
|
||||||
|
虚拟环境不会复制整个操作系统,也不会删除系统 Python。它只是隔离 Python 解释器入口和软件包目录。
|
||||||
|
|
||||||
|
## 六、创建虚拟环境
|
||||||
|
|
||||||
|
本项目统一把虚拟环境放在项目根目录的 `.venv` 中。
|
||||||
|
|
||||||
|
先在 PowerShell 进入项目根目录:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd D:\Code\Python
|
||||||
|
```
|
||||||
|
|
||||||
|
创建环境:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
逐段理解:
|
||||||
|
|
||||||
|
- `python`:使用当前 Python;
|
||||||
|
- `-m venv`:运行标准库中的 `venv` 模块;
|
||||||
|
- `.venv`:创建的环境目录名称。
|
||||||
|
|
||||||
|
创建可能需要几秒钟。成功后根目录会出现 `.venv`,但项目根目录的 `.gitignore` 已忽略它。
|
||||||
|
|
||||||
|
## 七、激活虚拟环境
|
||||||
|
|
||||||
|
PowerShell 中执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
激活后,命令提示符前通常会出现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
(.venv)
|
||||||
|
```
|
||||||
|
|
||||||
|
激活的作用主要是临时调整当前终端的命令查找顺序,让 `python` 和 `pip` 优先指向 `.venv`。
|
||||||
|
|
||||||
|
激活只影响当前终端窗口。关闭终端后,下次需要重新激活。
|
||||||
|
|
||||||
|
## 八、遇到 PowerShell 脚本限制怎么办
|
||||||
|
|
||||||
|
部分机器可能阻止运行 `Activate.ps1`。本课不要求为了激活环境而修改整台电脑的执行策略。
|
||||||
|
|
||||||
|
即使不激活,也可以直接使用虚拟环境中的 Python:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe --version
|
||||||
|
.\.venv\Scripts\python.exe .\02_python进阶\2_8_虚拟环境与依赖管理\environment_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
这种写法目标明确,也能保证使用 `.venv`。如果遇到执行策略错误,保留英文错误原文,再说明你执行的命令,我会帮助判断。
|
||||||
|
|
||||||
|
## 九、确认正在使用哪个 Python
|
||||||
|
|
||||||
|
激活前后都可以执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -c "import sys; print(sys.executable)"
|
||||||
|
```
|
||||||
|
|
||||||
|
激活虚拟环境后,输出路径应指向项目中的 `.venv\Scripts\python.exe`。
|
||||||
|
|
||||||
|
还可以执行本课示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_8_虚拟环境与依赖管理\environment_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例通过比较:
|
||||||
|
|
||||||
|
```python
|
||||||
|
sys.prefix != sys.base_prefix
|
||||||
|
```
|
||||||
|
|
||||||
|
判断当前是否处于普通 `venv` 虚拟环境。
|
||||||
|
|
||||||
|
## 十、退出虚拟环境
|
||||||
|
|
||||||
|
激活后执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
deactivate
|
||||||
|
```
|
||||||
|
|
||||||
|
退出只会恢复当前终端的命令环境,不会删除 `.venv`。
|
||||||
|
|
||||||
|
以后重新使用时再次运行激活命令即可。
|
||||||
|
|
||||||
|
## 十一、什么是 `pip`
|
||||||
|
|
||||||
|
`pip` 是 Python 常用的软件包安装工具。它可以从软件包仓库下载和管理第三方依赖。
|
||||||
|
|
||||||
|
本课推荐始终使用:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip 命令
|
||||||
|
```
|
||||||
|
|
||||||
|
而不是只写:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pip 命令
|
||||||
|
```
|
||||||
|
|
||||||
|
`python -m pip` 能更明确地表示:“使用当前这个 Python 对应的 pip”,减少多个 Python 环境之间装错位置的风险。
|
||||||
|
|
||||||
|
## 十二、查看依赖
|
||||||
|
|
||||||
|
查看 pip 版本和安装位置:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip --version
|
||||||
|
```
|
||||||
|
|
||||||
|
查看当前环境已安装的软件包:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip list
|
||||||
|
```
|
||||||
|
|
||||||
|
查看某个已安装软件包的信息:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip show 软件包名称
|
||||||
|
```
|
||||||
|
|
||||||
|
检查已安装依赖之间是否存在已知的不兼容:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十三、安装与卸载依赖
|
||||||
|
|
||||||
|
安装命令格式:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install 软件包名称
|
||||||
|
```
|
||||||
|
|
||||||
|
安装指定版本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install 软件包名称==版本号
|
||||||
|
```
|
||||||
|
|
||||||
|
卸载:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip uninstall 软件包名称
|
||||||
|
```
|
||||||
|
|
||||||
|
安装和卸载会修改当前 Python 环境。执行前必须先确认解释器路径,避免改动错误环境。本课不强制安装任何第三方依赖,也不要把示例中的占位名称原样执行。
|
||||||
|
|
||||||
|
## 十四、什么是版本固定
|
||||||
|
|
||||||
|
版本固定(Version Pinning)表示明确记录项目使用的依赖版本:
|
||||||
|
|
||||||
|
```text
|
||||||
|
some-package==1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
它可以减少不同机器安装到不同版本而产生的差异。
|
||||||
|
|
||||||
|
需要注意:版本号必须来自项目实际验证过的环境,不要随意照抄教程中的旧版本。以后开始 FastAPI、Django 项目时,会根据实际环境选择并验证版本。
|
||||||
|
|
||||||
|
## 十五、使用 `requirements.txt`
|
||||||
|
|
||||||
|
`requirements.txt` 是常见的依赖清单文件,每行记录一个直接依赖或带版本的依赖:
|
||||||
|
|
||||||
|
```text
|
||||||
|
package-a==1.2.3
|
||||||
|
package-b==4.5.6
|
||||||
|
```
|
||||||
|
|
||||||
|
在另一台机器或新虚拟环境中恢复依赖:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
根据当前环境导出所有已安装包:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip freeze > requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
`pip freeze` 会记录当前环境中的全部软件包,包括间接依赖。导出前要确认环境干净、确实属于当前项目,避免把无关包写入清单。
|
||||||
|
|
||||||
|
本课没有第三方依赖,因此不会提前创建包含虚假依赖的 `requirements.txt`。
|
||||||
|
|
||||||
|
## 十六、不要提交 `.venv`
|
||||||
|
|
||||||
|
`.venv` 可能包含大量文件,并带有当前机器的路径和平台信息,不适合提交到 Git。
|
||||||
|
|
||||||
|
应该提交的是:
|
||||||
|
|
||||||
|
- Python 源代码;
|
||||||
|
- 课程文档;
|
||||||
|
- 经过确认的依赖清单。
|
||||||
|
|
||||||
|
不应该提交的是:
|
||||||
|
|
||||||
|
- `.venv/`;
|
||||||
|
- `__pycache__/`;
|
||||||
|
- 本地密钥和 `.env`。
|
||||||
|
|
||||||
|
本项目根目录 `.gitignore` 已包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十七、完整示例
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
02_python进阶/2_8_虚拟环境与依赖管理/environment_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
它只读取并输出:
|
||||||
|
|
||||||
|
- Python 版本;
|
||||||
|
- 当前解释器路径;
|
||||||
|
- 当前环境目录名称;
|
||||||
|
- 是否为普通 `venv` 虚拟环境;
|
||||||
|
- 当前环境的 pip 版本。
|
||||||
|
|
||||||
|
示例不会创建环境,也不会安装或卸载依赖。
|
||||||
|
|
||||||
|
## 十八、运行方法
|
||||||
|
|
||||||
|
未激活虚拟环境时运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_8_虚拟环境与依赖管理\environment_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
创建环境后,不激活而直接运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe .\02_python进阶\2_8_虚拟环境与依赖管理\environment_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成代码练习后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\02_python进阶\2_8_虚拟环境与依赖管理\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十九、预期结果
|
||||||
|
|
||||||
|
具体路径和版本因机器而异,格式类似:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Python 版本:3.x.x
|
||||||
|
Python 解释器:某个实际路径
|
||||||
|
当前环境目录:某个环境名称
|
||||||
|
是否为 venv 虚拟环境:是或否
|
||||||
|
pip 版本:某个实际版本
|
||||||
|
```
|
||||||
|
|
||||||
|
在普通环境中通常显示“否”;使用 `.venv\Scripts\python.exe` 运行时应显示“是”。
|
||||||
|
|
||||||
|
## 二十、常见错误
|
||||||
|
|
||||||
|
### 20.1 创建环境后仍然使用原 Python
|
||||||
|
|
||||||
|
创建 `.venv` 不等于已经切换环境。需要激活,或者直接调用 `.venv\Scripts\python.exe`。
|
||||||
|
|
||||||
|
### 20.2 把依赖安装到错误环境
|
||||||
|
|
||||||
|
安装前先执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -c "import sys; print(sys.executable)"
|
||||||
|
```
|
||||||
|
|
||||||
|
确认路径后再使用 `python -m pip install`。
|
||||||
|
|
||||||
|
### 20.3 把 `.venv` 提交到 Git
|
||||||
|
|
||||||
|
虚拟环境是本地生成内容,应由 `.gitignore` 忽略。其他机器根据依赖清单重新创建。
|
||||||
|
|
||||||
|
### 20.4 把依赖清单当成虚拟环境
|
||||||
|
|
||||||
|
`requirements.txt` 只记录依赖名称和版本,不包含已安装文件。仍需先创建环境,再安装清单。
|
||||||
|
|
||||||
|
### 20.5 认为激活会永久改变系统
|
||||||
|
|
||||||
|
激活主要影响当前终端。执行 `deactivate` 或关闭终端即可退出。
|
||||||
|
|
||||||
|
### 20.6 随意修改 PowerShell 执行策略
|
||||||
|
|
||||||
|
激活脚本被阻止时,可以直接使用 `.venv\Scripts\python.exe`,本课不要求修改系统级策略。
|
||||||
|
|
||||||
|
## 二十一、课堂练习
|
||||||
|
|
||||||
|
练习分为两部分。
|
||||||
|
|
||||||
|
终端操作:
|
||||||
|
|
||||||
|
1. 在普通环境运行 `environment_example.py` 并记录结果;
|
||||||
|
2. 在项目根目录创建 `.venv`;
|
||||||
|
3. 激活环境,或直接使用 `.venv\Scripts\python.exe`;
|
||||||
|
4. 再次运行示例并比较解释器路径;
|
||||||
|
5. 执行 `python -m pip --version` 和 `python -m pip list`;
|
||||||
|
6. 退出环境并确认解释器恢复。
|
||||||
|
|
||||||
|
代码练习:打开 `practice.py`,完成环境判断、版本读取、环境报告和输出函数。
|
||||||
|
|
||||||
|
## 二十二、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。完成后,我会验证函数行为和类型注解,并分别在当前解释器与临时虚拟环境中运行,确认环境判断能够反映真实差异。
|
||||||
|
|
||||||
|
## 二十三、本课小结
|
||||||
|
|
||||||
|
- 虚拟环境为项目隔离 Python 解释器入口和依赖;
|
||||||
|
- 使用 `python -m venv .venv` 创建环境;
|
||||||
|
- 激活后,当前终端优先使用虚拟环境;
|
||||||
|
- 不激活时也可以直接调用虚拟环境中的 Python;
|
||||||
|
- `python -m pip` 能减少把依赖装错环境的风险;
|
||||||
|
- `requirements.txt` 用于记录和恢复依赖;
|
||||||
|
- 版本固定有助于保持不同机器的环境一致;
|
||||||
|
- `.venv` 是本地生成目录,不应提交到 Git。
|
||||||
|
|
||||||
|
## 二十四、验收标准
|
||||||
|
|
||||||
|
- 能解释虚拟环境解决的问题;
|
||||||
|
- 能创建、进入和退出 `.venv`;
|
||||||
|
- 能确认当前 Python 解释器路径;
|
||||||
|
- 能使用 `python -m pip` 查看依赖;
|
||||||
|
- 能说明安装与卸载会修改当前环境;
|
||||||
|
- 能解释 `requirements.txt` 的用途;
|
||||||
|
- 能说明版本固定的基本作用;
|
||||||
|
- 能解释为什么 `.venv` 不应提交;
|
||||||
|
- 环境检查脚本在普通环境与 `.venv` 中显示不同判断;
|
||||||
|
- 没有把个人绝对路径或虚假依赖写入项目文件。
|
||||||
45
02_python进阶/2_8_虚拟环境与依赖管理/environment_example.py
Normal file
45
02_python进阶/2_8_虚拟环境与依赖管理/environment_example.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 第 2-8 课完整示例:虚拟环境与依赖管理
|
||||||
|
#
|
||||||
|
# 本示例只读取当前 Python 解释器和环境信息,不安装、卸载任何依赖。
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def is_virtual_environment() -> bool:
|
||||||
|
"""判断当前程序是否运行在普通 venv 虚拟环境中。"""
|
||||||
|
# 普通环境中 sys.prefix 和 sys.base_prefix 通常相同。
|
||||||
|
# venv 激活并使用其 Python 后,这两个路径通常不同。
|
||||||
|
return sys.prefix != sys.base_prefix
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_version(package_name: str) -> str | None:
|
||||||
|
"""返回已安装依赖的版本,未安装时返回 None。"""
|
||||||
|
try:
|
||||||
|
return version(package_name)
|
||||||
|
except PackageNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_environment_name() -> str:
|
||||||
|
"""返回当前环境目录名称。"""
|
||||||
|
return Path(sys.prefix).name
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""输出解释器、虚拟环境和 pip 版本信息。"""
|
||||||
|
in_virtual_environment = is_virtual_environment()
|
||||||
|
environment_status = "是" if in_virtual_environment else "否"
|
||||||
|
pip_version = get_package_version("pip")
|
||||||
|
|
||||||
|
print(f"Python 版本:{platform.python_version()}")
|
||||||
|
print(f"Python 解释器:{sys.executable}")
|
||||||
|
print(f"当前环境目录:{get_environment_name()}")
|
||||||
|
print(f"是否为 venv 虚拟环境:{environment_status}")
|
||||||
|
print(f"pip 版本:{pip_version}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
78
02_python进阶/2_8_虚拟环境与依赖管理/practice.py
Normal file
78
02_python进阶/2_8_虚拟环境与依赖管理/practice.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# 第 2-8 课课堂练习:检查虚拟环境
|
||||||
|
#
|
||||||
|
# 本文件只读取环境信息,不负责创建环境或安装依赖。
|
||||||
|
# 创建、激活和退出虚拟环境的操作请按照讲义在 PowerShell 中完成。
|
||||||
|
# 不要删除题目、预期结果和自查注释。
|
||||||
|
|
||||||
|
# 第一部分:定义 is_virtual_environment() 函数
|
||||||
|
# 1. 不接收参数,返回值标注为 bool。
|
||||||
|
# 2. 比较 sys.prefix 和 sys.base_prefix。
|
||||||
|
# 3. 两者不相等时返回 True,否则返回 False。
|
||||||
|
# 4. 不要根据路径中是否包含“.venv”来判断。
|
||||||
|
|
||||||
|
# 第二部分:定义 get_python_version() 函数
|
||||||
|
# 1. 不接收参数,返回值标注为 str。
|
||||||
|
# 2. 调用 platform.python_version() 并 return 结果。
|
||||||
|
# 3. 不要手工写死版本号,因为不同机器的版本可能不同。
|
||||||
|
|
||||||
|
# 第三部分:定义 get_environment_name() 函数
|
||||||
|
# 1. 不接收参数,返回值标注为 str。
|
||||||
|
# 2. 使用 Path(sys.prefix).name 取得当前环境目录名称。
|
||||||
|
# 3. return 得到的名称。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:定义 get_environment_report() 函数
|
||||||
|
# 1. 不接收参数,返回值标注为 dict[str, str]。
|
||||||
|
# 2. 调用前面三个函数。
|
||||||
|
# 3. 把布尔值转换为中文:“是”或“否”。
|
||||||
|
# 4. return 包含以下三个键的字典:
|
||||||
|
# "python_version":当前 Python 版本;
|
||||||
|
# "environment_name":当前环境目录名称;
|
||||||
|
# "is_virtual_environment":中文“是”或“否”。
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:定义 print_environment_report(report) 函数
|
||||||
|
# 1. 参数标注为 dict[str, str],返回值标注为 None。
|
||||||
|
# 2. 依次输出:
|
||||||
|
# Python 版本:实际版本
|
||||||
|
# 当前环境:实际环境名称
|
||||||
|
# 是否为虚拟环境:是或否
|
||||||
|
# 3. 本函数只负责输出,不修改 report。
|
||||||
|
|
||||||
|
|
||||||
|
# 第六部分:定义 main() 函数
|
||||||
|
# 1. 返回值标注为 None。
|
||||||
|
# 2. 调用 get_environment_report() 并保存结果。
|
||||||
|
# 3. 调用 print_environment_report() 输出报告。
|
||||||
|
# 4. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||||
|
|
||||||
|
|
||||||
|
# 第七部分:在两个环境中运行
|
||||||
|
# 1. 未激活本课虚拟环境时运行一次 practice.py,记录输出。
|
||||||
|
# 2. 按讲义创建并激活项目根目录下的 .venv。
|
||||||
|
# 3. 再次运行 practice.py。
|
||||||
|
# 4. 对比两次的环境名称和“是否为虚拟环境”。
|
||||||
|
# 5. 两台机器的路径和 Python 版本可能不同,不要求写死相同结果。
|
||||||
|
|
||||||
|
|
||||||
|
# 最终验收测试:
|
||||||
|
# 1. 所有函数的参数和返回值注解正确;
|
||||||
|
# 2. Python 版本来自 platform.python_version();
|
||||||
|
# 3. 环境名称来自 Path(sys.prefix).name;
|
||||||
|
# 4. 虚拟环境判断来自 sys.prefix != sys.base_prefix;
|
||||||
|
# 5. 报告字典包含三个指定键,值都是字符串;
|
||||||
|
# 6. 输出函数不修改报告字典;
|
||||||
|
# 7. 普通环境与 .venv 中的输出能够体现环境差异;
|
||||||
|
# 8. 没有在代码中写死个人机器路径或 Python 版本;
|
||||||
|
# 9. 没有在脚本中自动安装或卸载依赖。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否知道虚拟环境用于隔离项目依赖;
|
||||||
|
# 2. 是否知道激活环境不会删除系统 Python;
|
||||||
|
# 3. 是否优先使用 python -m pip;
|
||||||
|
# 4. 是否知道 requirements.txt 用于记录项目依赖;
|
||||||
|
# 5. 是否知道 .venv 不应提交到 Git;
|
||||||
|
# 6. 是否能退出环境并重新激活;
|
||||||
|
# 7. 是否保留了完整题目和验收说明。
|
||||||
405
02_python进阶/2_8_虚拟环境与依赖管理/补充_conda_pip_uv与虚拟环境的区别.md
Normal file
405
02_python进阶/2_8_虚拟环境与依赖管理/补充_conda_pip_uv与虚拟环境的区别.md
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
# 补充:Conda、pip、uv 与 Python 虚拟环境的区别
|
||||||
|
|
||||||
|
## 一、先给出结论
|
||||||
|
|
||||||
|
这些名称不完全属于同一类工具:
|
||||||
|
|
||||||
|
- `venv`、`virtualenv`:主要负责创建相互隔离的 Python 环境;
|
||||||
|
- `pip`:主要负责向某个 Python 环境安装和卸载 Python 软件包,本身不负责创建虚拟环境;
|
||||||
|
- Conda:同时管理环境、Python 版本、Python 包和部分非 Python 软件;
|
||||||
|
- `uv`:同时覆盖 Python 版本、虚拟环境、依赖安装、项目依赖和锁文件等工作。
|
||||||
|
|
||||||
|
因此,“Conda、pip、uv 哪个虚拟环境更好”这个问题并不完全准确。更合适的问题是:
|
||||||
|
|
||||||
|
> 当前项目应该使用哪一种环境管理与依赖管理组合?
|
||||||
|
|
||||||
|
## 二、几个概念不要混在一起
|
||||||
|
|
||||||
|
### 2.1 Python 解释器
|
||||||
|
|
||||||
|
Python 解释器是实际运行 `.py` 文件的程序,例如当前机器检测到的:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\ProgramData\miniconda3\python.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
不同环境可能有自己的解释器入口,也可能使用不同的 Python 版本。
|
||||||
|
|
||||||
|
### 2.2 虚拟环境
|
||||||
|
|
||||||
|
虚拟环境负责隔离项目使用的 Python 和已安装软件包。例如项目 A 与项目 B 可以分别拥有自己的 `.venv`,互不影响。
|
||||||
|
|
||||||
|
### 2.3 包管理器
|
||||||
|
|
||||||
|
包管理器负责查找、安装、更新和卸载软件包:
|
||||||
|
|
||||||
|
- `pip` 主要管理 Python 软件包;
|
||||||
|
- Conda 可以管理 Conda 软件包,其中可以包含 Python、Python 包以及其他软件;
|
||||||
|
- `uv` 可以通过项目工作流或兼容 pip 的命令管理 Python 依赖。
|
||||||
|
|
||||||
|
### 2.4 依赖解析与锁定
|
||||||
|
|
||||||
|
依赖解析负责计算“安装哪些版本才能彼此兼容”。锁文件用于记录解析后的精确结果,使其他机器尽量复现相同环境。
|
||||||
|
|
||||||
|
常见文件包括:
|
||||||
|
|
||||||
|
- pip 工作流:`requirements.txt`;
|
||||||
|
- Conda 工作流:`environment.yml`,也可配合相应锁文件;
|
||||||
|
- uv 项目工作流:`pyproject.toml` 和 `uv.lock`。
|
||||||
|
|
||||||
|
## 三、`venv`:Python 自带的基础选择
|
||||||
|
|
||||||
|
`venv` 是 Python 标准库中的虚拟环境工具,不需要单独安装。
|
||||||
|
|
||||||
|
创建环境:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
激活:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
在环境中安装包:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install 软件包名称
|
||||||
|
```
|
||||||
|
|
||||||
|
特点:
|
||||||
|
|
||||||
|
- Python 自带,概念简单;
|
||||||
|
- 通常基于创建它的现有 Python;
|
||||||
|
- 只负责创建环境,不负责完整项目锁定;
|
||||||
|
- 通常与 `pip`、`requirements.txt` 一起使用;
|
||||||
|
- 适合普通 Python 脚本、Web 后端和入门学习。
|
||||||
|
|
||||||
|
典型组合:
|
||||||
|
|
||||||
|
```text
|
||||||
|
venv 创建环境
|
||||||
|
+ pip 安装依赖
|
||||||
|
+ requirements.txt 记录依赖
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、`virtualenv`:功能更丰富的环境创建工具
|
||||||
|
|
||||||
|
`virtualenv` 是需要额外安装的第三方工具。Python 官方打包指南说明,`venv` 是 Python 自带方案,而 `virtualenv` 提供更广的兼容性和额外能力。
|
||||||
|
|
||||||
|
基本命令:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
virtualenv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
特点:
|
||||||
|
|
||||||
|
- 主要职责与 `venv` 相同,都是创建隔离环境;
|
||||||
|
- 需要先安装;
|
||||||
|
- 提供更丰富的解释器选择、配置和兼容能力;
|
||||||
|
- 在只需要基础环境隔离的新项目中,通常先用 `venv` 就够了。
|
||||||
|
|
||||||
|
对当前零基础课程,不必同时学习 `venv` 和 `virtualenv`。先理解 `venv`,以后遇到明确需求再使用 `virtualenv`。
|
||||||
|
|
||||||
|
## 五、`pip`:软件包安装器,不是虚拟环境
|
||||||
|
|
||||||
|
`pip` 的职责是把 Python 软件包安装到某个环境。
|
||||||
|
|
||||||
|
推荐命令:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install 软件包名称
|
||||||
|
```
|
||||||
|
|
||||||
|
这里的关键是“当前这个 Python”:
|
||||||
|
|
||||||
|
- 如果当前 Python 来自 `.venv`,依赖会装入 `.venv`;
|
||||||
|
- 如果当前 Python 来自 Conda 环境,依赖会装入该 Conda 环境;
|
||||||
|
- 如果当前 Python 是全局环境,依赖可能被装入全局环境。
|
||||||
|
|
||||||
|
所以 `pip` 自己不提供隔离,隔离由 `venv`、`virtualenv` 或 Conda 环境等机制提供。
|
||||||
|
|
||||||
|
`pip` 常见能力:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install 软件包名称
|
||||||
|
python -m pip uninstall 软件包名称
|
||||||
|
python -m pip list
|
||||||
|
python -m pip show 软件包名称
|
||||||
|
python -m pip check
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
优点:
|
||||||
|
|
||||||
|
- Python 生态中最通用;
|
||||||
|
- PyPI 上的软件包覆盖广;
|
||||||
|
- 与 `venv` 配合简单;
|
||||||
|
- 教程、部署平台和持续集成环境普遍支持。
|
||||||
|
|
||||||
|
需要注意:
|
||||||
|
|
||||||
|
- 安装前必须确认当前解释器;
|
||||||
|
- `requirements.txt` 与环境本身是两回事;
|
||||||
|
- `pip freeze` 会导出当前环境全部软件包,环境不干净时可能带入无关依赖。
|
||||||
|
|
||||||
|
## 六、Conda:环境管理器加跨语言包管理器
|
||||||
|
|
||||||
|
Conda 同时负责环境和包管理。它可以在创建环境时指定 Python 版本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda create -n python-course python=3.13
|
||||||
|
conda activate python-course
|
||||||
|
```
|
||||||
|
|
||||||
|
安装 Conda 软件包:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda install 软件包名称
|
||||||
|
```
|
||||||
|
|
||||||
|
查看环境:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda env list
|
||||||
|
```
|
||||||
|
|
||||||
|
Conda 环境与普通 `venv` 的重要区别:
|
||||||
|
|
||||||
|
- Conda 可以直接管理环境中的 Python 版本;
|
||||||
|
- Conda 包不局限于纯 Python 包,也能包含本地库、命令行程序等内容;
|
||||||
|
- 软件包来自 Conda channel,例如 `defaults` 或 `conda-forge`;
|
||||||
|
- 环境通常集中保存在 Conda 安装目录的 `envs` 下,也可以使用指定路径;
|
||||||
|
- 环境描述常使用 `environment.yml`。
|
||||||
|
|
||||||
|
Conda 更适合:
|
||||||
|
|
||||||
|
- 数据科学、机器学习和科学计算;
|
||||||
|
- 依赖 NumPy、PyTorch、CUDA 或本地二进制库的项目;
|
||||||
|
- 需要同时管理 Python 与非 Python 软件的环境;
|
||||||
|
- 已经以 Anaconda、Miniconda 或 Miniforge 作为主要 Python 发行方式的团队。
|
||||||
|
|
||||||
|
代价和注意事项:
|
||||||
|
|
||||||
|
- 工具体系比 `venv + pip` 更大;
|
||||||
|
- channel 和包来源会影响解析结果;
|
||||||
|
- Conda 包版本可能与 PyPI 发布节奏不同;
|
||||||
|
- 同一环境混用 Conda 与 pip 时需要保持清晰边界。
|
||||||
|
|
||||||
|
## 七、Conda 环境中能不能使用 pip
|
||||||
|
|
||||||
|
可以,但建议遵守以下顺序:
|
||||||
|
|
||||||
|
1. 创建并激活独立 Conda 环境;
|
||||||
|
2. 优先一次性安装需要的 Conda 包;
|
||||||
|
3. 确认使用的是当前环境中的 pip;
|
||||||
|
4. 只有 Conda channel 没有需要的软件包时,再使用 pip;
|
||||||
|
5. pip 安装后如果还要大规模调整 Conda 依赖,优先考虑重建环境,而不是反复混装。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda create -n python-course python=3.13 pip
|
||||||
|
conda activate python-course
|
||||||
|
python -c "import sys; print(sys.executable)"
|
||||||
|
python -m pip --version
|
||||||
|
```
|
||||||
|
|
||||||
|
然后才能确认 pip 指向当前 Conda 环境。
|
||||||
|
|
||||||
|
不建议在一个环境中无计划地交替执行大量 `conda install` 和 `pip install`。两套工具使用不同的软件包格式与依赖信息,复杂项目可能出现一方不知道另一方具体修改了什么的问题。
|
||||||
|
|
||||||
|
## 八、`uv`:速度较快的一体化 Python 工具
|
||||||
|
|
||||||
|
`uv` 是独立工具,不依赖当前 Python 才能启动。它覆盖多项工作:
|
||||||
|
|
||||||
|
- 安装和选择 Python;
|
||||||
|
- 创建虚拟环境;
|
||||||
|
- 安装与卸载 Python 软件包;
|
||||||
|
- 管理 `pyproject.toml` 项目;
|
||||||
|
- 生成并使用 `uv.lock`;
|
||||||
|
- 同步项目环境;
|
||||||
|
- 在项目环境中运行命令。
|
||||||
|
|
||||||
|
仅创建虚拟环境:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
使用类似 pip 的接口:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv pip install 软件包名称
|
||||||
|
uv pip list
|
||||||
|
uv pip check
|
||||||
|
```
|
||||||
|
|
||||||
|
完整项目工作流:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv init
|
||||||
|
uv add 软件包名称
|
||||||
|
uv sync
|
||||||
|
uv run python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
在项目工作流中:
|
||||||
|
|
||||||
|
- `pyproject.toml` 声明项目及直接依赖;
|
||||||
|
- `uv.lock` 记录解析后的精确依赖;
|
||||||
|
- 默认项目环境通常是 `.venv`;
|
||||||
|
- `uv run` 会在运行前确认锁文件和项目环境保持同步。
|
||||||
|
|
||||||
|
官方文档建议:使用 uv 项目工作流时,通过 `uv add` 管理项目依赖,不要把 `uv pip install` 当作项目依赖声明方式。`uv pip` 更适合兼容传统 pip 工作流或手动管理环境。
|
||||||
|
|
||||||
|
## 九、`uv` 与 pip 的关系
|
||||||
|
|
||||||
|
`uv pip` 提供与 pip 类似的命令体验,但 `uv` 并不是 pip 的插件,也不是简单调用 pip。
|
||||||
|
|
||||||
|
可以这样理解:
|
||||||
|
|
||||||
|
```text
|
||||||
|
传统方式:venv + pip + requirements.txt
|
||||||
|
uv 兼容方式:uv venv + uv pip + requirements.txt
|
||||||
|
uv 项目方式:uv + pyproject.toml + uv.lock + .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
如果使用完整 uv 项目工作流,常用命令通常是:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv add
|
||||||
|
uv remove
|
||||||
|
uv sync
|
||||||
|
uv run
|
||||||
|
```
|
||||||
|
|
||||||
|
而不是手动激活环境后不断执行 `uv pip install`。
|
||||||
|
|
||||||
|
## 十、`uv` 与 Conda 的区别
|
||||||
|
|
||||||
|
两者都能管理 Python 和环境,但侧重点不同。
|
||||||
|
|
||||||
|
Conda:
|
||||||
|
|
||||||
|
- 以 Conda 包、channel 和 Conda 环境为核心;
|
||||||
|
- 能管理更广泛的非 Python 依赖;
|
||||||
|
- 在科学计算、数据分析和机器学习生态中常见;
|
||||||
|
- 可以管理 Python、R、系统库和二进制程序等内容。
|
||||||
|
|
||||||
|
uv:
|
||||||
|
|
||||||
|
- 以现代 Python 项目、PyPI、`pyproject.toml` 和锁文件为核心;
|
||||||
|
- 默认使用项目内 `.venv`;
|
||||||
|
- 适合普通 Python 应用、库、工具和 Web 后端;
|
||||||
|
- 依赖操作和环境同步速度通常是其主要优势之一。
|
||||||
|
|
||||||
|
如果项目强依赖 CUDA、本地科学计算库或 Conda channel,Conda 通常更自然。如果项目主要是 PyPI 中的 Python 依赖,uv 项目工作流通常更轻量。
|
||||||
|
|
||||||
|
## 十一、核心对比表
|
||||||
|
|
||||||
|
| 工具 | 创建隔离环境 | 安装 Python 包 | 管理 Python 版本 | 管理非 Python 软件 | 典型依赖文件 | 适合场景 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `venv` | 是 | 否,通常配合 pip | 否,使用已有 Python | 否 | 无,常配合 `requirements.txt` | 入门、普通脚本、Web 项目 |
|
||||||
|
| `virtualenv` | 是 | 否,通常配合 pip | 选择已有解释器 | 否 | 无,常配合 `requirements.txt` | 需要更强环境创建能力 |
|
||||||
|
| `pip` | 否 | 是 | 否 | 通常不负责 | `requirements.txt` | 向现有 Python 环境安装包 |
|
||||||
|
| Conda | 是 | 是 | 是 | 是 | `environment.yml` 等 | 数据科学、机器学习、二进制依赖 |
|
||||||
|
| `uv` | 是 | 是 | 是 | 主要面向 Python 生态 | `pyproject.toml`、`uv.lock` | 现代 Python 项目和 Web 后端 |
|
||||||
|
|
||||||
|
## 十二、应该如何选择
|
||||||
|
|
||||||
|
### 12.1 零基础学习和理解原理
|
||||||
|
|
||||||
|
优先学习:
|
||||||
|
|
||||||
|
```text
|
||||||
|
venv + pip
|
||||||
|
```
|
||||||
|
|
||||||
|
原因是它们能清楚展示“环境隔离”和“包安装”是两个不同职责,也是 Python 官方文档中的基础工作流。
|
||||||
|
|
||||||
|
### 12.2 普通 Python、FastAPI 或 Django 项目
|
||||||
|
|
||||||
|
可以选择:
|
||||||
|
|
||||||
|
```text
|
||||||
|
venv + pip
|
||||||
|
```
|
||||||
|
|
||||||
|
或者:
|
||||||
|
|
||||||
|
```text
|
||||||
|
uv 项目工作流
|
||||||
|
```
|
||||||
|
|
||||||
|
前者通用、容易理解;后者在依赖解析、锁定和同步方面更加一体化。
|
||||||
|
|
||||||
|
### 12.3 数据科学和机器学习
|
||||||
|
|
||||||
|
如果涉及复杂科学计算、CUDA 或系统二进制依赖,可以优先考虑:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Conda
|
||||||
|
```
|
||||||
|
|
||||||
|
必要时在独立 Conda 环境中谨慎补充 pip 包。
|
||||||
|
|
||||||
|
### 12.4 当前教学项目
|
||||||
|
|
||||||
|
当前机器的 `python` 来自 Miniconda,但这不等于每个项目都必须使用 Conda 环境。
|
||||||
|
|
||||||
|
为了理解基础概念,本课程讲义使用 `venv + pip`。以后进入 FastAPI 和 Django 阶段,可以根据学习目标二选一:
|
||||||
|
|
||||||
|
- 继续使用 `venv + pip`,保持通用和透明;
|
||||||
|
- 切换到 uv 项目工作流,学习现代依赖锁定与同步。
|
||||||
|
|
||||||
|
不建议在同一个项目中同时维护 `requirements.txt`、`environment.yml` 和 `uv.lock` 三套互不一致的依赖事实来源。应该选定一种主要工作流。
|
||||||
|
|
||||||
|
## 十三、常见误区
|
||||||
|
|
||||||
|
### 13.1 “pip 创建了虚拟环境”
|
||||||
|
|
||||||
|
不正确。通常是 `venv` 创建环境,pip 向环境安装软件包。
|
||||||
|
|
||||||
|
### 13.2 “激活环境后就安装了依赖”
|
||||||
|
|
||||||
|
不正确。激活只切换当前终端优先使用的解释器和工具,依赖仍需单独安装或同步。
|
||||||
|
|
||||||
|
### 13.3 “Conda 环境就是 Python venv”
|
||||||
|
|
||||||
|
两者都能实现隔离,但内部结构、包格式、依赖来源和管理范围不同,不能简单视为同一个实现。
|
||||||
|
|
||||||
|
### 13.4 “uv pip 就是速度更快的 pip 命令别名”
|
||||||
|
|
||||||
|
不准确。它提供兼容 pip 的操作方式,但 uv 是独立实现,并且还有完整的项目、Python、环境和锁文件管理能力。
|
||||||
|
|
||||||
|
### 13.5 “有锁文件就不需要虚拟环境”
|
||||||
|
|
||||||
|
不正确。锁文件描述应该安装什么,虚拟环境保存实际安装结果,两者职责不同。
|
||||||
|
|
||||||
|
### 13.6 “把 `.venv` 复制到另一台机器就能复用”
|
||||||
|
|
||||||
|
不推荐。虚拟环境可能包含绝对路径和平台相关文件。应在新机器重新创建环境,并根据依赖文件安装或同步。
|
||||||
|
|
||||||
|
## 十四、推荐记忆方式
|
||||||
|
|
||||||
|
```text
|
||||||
|
venv / virtualenv:创建隔离房间
|
||||||
|
pip:向房间里安装 Python 包
|
||||||
|
Conda:创建房间,并管理 Python 与更广的软件
|
||||||
|
uv:创建房间,并管理现代 Python 项目、依赖和锁文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十五、官方资料
|
||||||
|
|
||||||
|
- [Python 打包指南:使用 pip 和 venv](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/)
|
||||||
|
- [Python 打包指南:安装软件包](https://packaging.python.org/en/latest/tutorials/installing-packages/)
|
||||||
|
- [virtualenv 官方文档](https://virtualenv.pypa.io/en/stable/)
|
||||||
|
- [Conda:管理环境](https://docs.conda.io/projects/conda/en/stable/user-guide/tasks/manage-environments.html)
|
||||||
|
- [Conda:管理软件包](https://docs.conda.io/projects/conda/en/stable/user-guide/tasks/manage-pkgs.html)
|
||||||
|
- [uv:Python 环境](https://docs.astral.sh/uv/pip/environments/)
|
||||||
|
- [uv:项目结构、环境与锁文件](https://docs.astral.sh/uv/concepts/projects/layout/)
|
||||||
|
- [uv:从 pip 工作流迁移到 uv 项目](https://docs.astral.sh/uv/guides/migration/pip-to-project/)
|
||||||
6
02_python进阶/2_9_python进阶综合项目/.gitignore
vendored
Normal file
6
02_python进阶/2_9_python进阶综合项目/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 运行完整示例时自动生成的本地任务数据。
|
||||||
|
task_data/
|
||||||
|
practice_data/
|
||||||
|
|
||||||
|
# Python 自动生成的缓存文件。
|
||||||
|
__pycache__/
|
||||||
167
02_python进阶/2_9_python进阶综合项目/README.md
Normal file
167
02_python进阶/2_9_python进阶综合项目/README.md
Normal 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
|
||||||
|
|
||||||
|
JSON(JavaScript 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 保存和读取的方向;
|
||||||
|
- 能说明本项目中异常处理、推导式、生成器、装饰器和类型注解分别在哪里使用;
|
||||||
|
- 能按题目完成三个练习模块;
|
||||||
|
- 再次运行练习程序时,能够读取上次保存的任务;
|
||||||
|
- 已完成第二阶段的综合项目,具备进入第三阶段“面向对象编程”的基础。
|
||||||
52
02_python进阶/2_9_python进阶综合项目/main.py
Normal file
52
02_python进阶/2_9_python进阶综合项目/main.py
Normal 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()
|
||||||
74
02_python进阶/2_9_python进阶综合项目/practice.py
Normal file
74
02_python进阶/2_9_python进阶综合项目/practice.py
Normal 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. 是否只保存当前课程目录中的练习数据。
|
||||||
69
02_python进阶/2_9_python进阶综合项目/task_service.py
Normal file
69
02_python进阶/2_9_python进阶综合项目/task_service.py
Normal 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),
|
||||||
|
}
|
||||||
33
02_python进阶/2_9_python进阶综合项目/task_store.py
Normal file
33
02_python进阶/2_9_python进阶综合项目/task_store.py
Normal 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")
|
||||||
259
03_面向对象/3_1_Python与Java的类和对象/README.md
Normal file
259
03_面向对象/3_1_Python与Java的类和对象/README.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# 第 3-1 课:Python 与 Java 的类和对象
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
你已经有成熟的 Java 开发经验,因此本阶段不再从“什么是对象”开始长篇讲解。本课采用差异驱动方式:先建立 Java 写法与 Python 写法的对应关系,再通过代码和练习形成 Python 语感。
|
||||||
|
|
||||||
|
即使没有完整阅读本文,也请至少运行示例并完成 `practice.py`。示例输出和练习题中会重复本课的关键差异。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 使用 Python 定义类、初始化对象并调用实例方法;
|
||||||
|
2. 理解 `self` 与 Java `this` 的关系和语法差异;
|
||||||
|
3. 区分实例属性与类属性;
|
||||||
|
4. 区分实例方法、类方法和静态方法;
|
||||||
|
5. 避免把 Java 的字段声明、构造器和 `new` 原样搬到 Python;
|
||||||
|
6. 理解 Python 对象模型更动态,但工程代码仍应保持清晰约束。
|
||||||
|
|
||||||
|
## 三、前置知识
|
||||||
|
|
||||||
|
- 已有 Java 类、对象、构造器、实例字段和 `static` 成员经验;
|
||||||
|
- 已学习 Python 函数、字典和类型注解;
|
||||||
|
- 能在 PowerShell 中运行 Python 文件。
|
||||||
|
|
||||||
|
## 四、先看差异速查表
|
||||||
|
|
||||||
|
| 关注点 | Java | Python |
|
||||||
|
|---|---|---|
|
||||||
|
| 定义类 | `public class Agent` | `class Agent:` |
|
||||||
|
| 创建对象 | `new Agent(...)` | `Agent(...)`,没有 `new` |
|
||||||
|
| 初始化入口 | 与类同名的构造器 | `__init__` 特殊方法 |
|
||||||
|
| 当前对象 | 隐式 `this` | 定义方法时显式写 `self` |
|
||||||
|
| 实例字段 | 通常在类体中声明 | 通常在 `__init__` 中赋值创建 |
|
||||||
|
| 类级数据 | `static` 字段 | 类属性 |
|
||||||
|
| 实例方法 | 默认接收隐式 `this` | 第一个参数通常写 `self` |
|
||||||
|
| 类级工厂 | 常用 `static` 方法 | 常用 `@classmethod` 和 `cls` |
|
||||||
|
| 静态工具方法 | `static` 方法 | `@staticmethod`,无 `self`/`cls` |
|
||||||
|
| 访问控制 | `public/protected/private` | 主要依赖命名约定,后续课详讲 |
|
||||||
|
| 方法重载 | 支持同名不同参数 | 不按 Java 方式支持,后续课详讲 |
|
||||||
|
|
||||||
|
## 五、最小类定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Agent:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def describe(self) -> str:
|
||||||
|
return f"Agent:{self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
agent = Agent("代码助手")
|
||||||
|
print(agent.describe())
|
||||||
|
```
|
||||||
|
|
||||||
|
对应 Java 思维可以理解为:
|
||||||
|
|
||||||
|
- `__init__` 负责接收初始化数据,但它不是与类同名的构造器;
|
||||||
|
- `self.name = name` 在当前实例上创建并赋值 `name` 属性;
|
||||||
|
- `self` 类似 `this`,只是 Python 要在方法定义中明确写出来;
|
||||||
|
- 调用 `agent.describe()` 时不传 `self`,解释器会自动传入 `agent`;
|
||||||
|
- 创建对象直接调用 `Agent(...)`,不写 `new`。
|
||||||
|
|
||||||
|
严格来说,Python 先通过 `__new__` 创建对象,再由 `__init__` 初始化对象。本阶段日常业务代码先掌握 `__init__` 即可,暂不展开 `__new__`。
|
||||||
|
|
||||||
|
## 六、Python 属性不要求预先声明
|
||||||
|
|
||||||
|
Java 通常先声明字段:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private String name;
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 常在 `__init__` 中直接建立实例属性:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.name = name
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 甚至允许对象创建后动态添加属性:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent.status = "工作中"
|
||||||
|
```
|
||||||
|
|
||||||
|
这体现了 Python 的动态性。它不是鼓励随意改变对象结构;在成熟项目中,仍应尽量在 `__init__` 中集中建立稳定的实例属性,让阅读者和类型检查工具更容易理解对象。
|
||||||
|
|
||||||
|
## 七、实例属性与类属性
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Agent:
|
||||||
|
platform = "Codex"
|
||||||
|
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
```
|
||||||
|
|
||||||
|
- `self.name` 是实例属性,每个对象可以保存不同值;
|
||||||
|
- `Agent.platform` 是类属性,由类统一提供,作用类似 Java `static` 字段;
|
||||||
|
- `agent.platform` 也能读取类属性,但类级含义明确时优先使用 `Agent.platform`;
|
||||||
|
- 如果执行 `agent.platform = "其他平台"`,通常会在该实例上创建同名实例属性,从而遮蔽类属性,而不是修改整个类的值。
|
||||||
|
|
||||||
|
最后一点与 Java 字段访问直觉不同,是常见问题。
|
||||||
|
|
||||||
|
## 八、三种方法
|
||||||
|
|
||||||
|
### 8.1 实例方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
def describe(self) -> str:
|
||||||
|
return self.name
|
||||||
|
```
|
||||||
|
|
||||||
|
通过 `self` 访问当前对象,适合处理实例状态。
|
||||||
|
|
||||||
|
### 8.2 类方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, config: dict[str, str]) -> "Agent":
|
||||||
|
return cls(config["name"])
|
||||||
|
```
|
||||||
|
|
||||||
|
装饰器(Decorator)`@classmethod` 会让方法接收当前类 `cls`。它很适合命名工厂:使用 `cls(...)` 而非写死 `Agent(...)`,子类继承时更自然。
|
||||||
|
|
||||||
|
### 8.3 静态方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_name(name: str) -> bool:
|
||||||
|
return bool(name.strip())
|
||||||
|
```
|
||||||
|
|
||||||
|
静态方法不接收 `self` 或 `cls`。它只是逻辑上属于该类,但不需要访问实例状态或类状态。
|
||||||
|
|
||||||
|
不要为了模仿 Java 工具类而大量使用静态方法。普通模块函数也是 Python 中很自然的组织方式。
|
||||||
|
|
||||||
|
## 九、完整示例
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例会直接输出五项差异:
|
||||||
|
|
||||||
|
1. `__init__` 与 Java 构造器的区别;
|
||||||
|
2. `self` 与 `this` 的区别;
|
||||||
|
3. Python 属性的动态创建;
|
||||||
|
4. 类属性与 `static` 字段的对应关系;
|
||||||
|
5. Python 创建对象不写 `new`。
|
||||||
|
|
||||||
|
## 十、运行方法
|
||||||
|
|
||||||
|
在项目根目录 `D:\Code\Python` 打开 PowerShell,运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\03_面向对象\3_1_Python与Java的类和对象\class_object_comparison.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成练习后运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\03_面向对象\3_1_Python与Java的类和对象\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十一、示例预期结果
|
||||||
|
|
||||||
|
输出中应依次看到“差异 1”到“差异 5”,并看到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
代码助手 使用 gpt-5.2,运行平台:Codex
|
||||||
|
动态添加的 status:工作中
|
||||||
|
通过类读取:Codex;通过实例读取:Codex
|
||||||
|
审查助手 使用 gpt-5,运行平台:Codex
|
||||||
|
名称是否合法:True
|
||||||
|
```
|
||||||
|
|
||||||
|
## 十二、Java 开发者常见误区
|
||||||
|
|
||||||
|
### 12.1 创建对象时写 `new`
|
||||||
|
|
||||||
|
Python 没有这种语法,直接写:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent = Agent("代码助手")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2 调用方法时手动传 `self`
|
||||||
|
|
||||||
|
调用 `agent.describe()` 时 Python 会自动绑定实例,不要写 `agent.describe(agent)`。
|
||||||
|
|
||||||
|
### 12.3 忘记在方法定义中写 `self`
|
||||||
|
|
||||||
|
实例方法定义必须显式接收当前对象:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def describe(self) -> str:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`self` 不是保留关键字,但它是整个 Python 社区遵守的标准命名,不要改成其他名字。
|
||||||
|
|
||||||
|
### 12.4 把所有工厂方法都写成静态方法
|
||||||
|
|
||||||
|
如果方法需要创建“当前类”的实例,优先考虑 `@classmethod` 和 `cls(...)`,这比写死类名更适合继承。
|
||||||
|
|
||||||
|
### 12.5 认为没有 `private` 就无法封装
|
||||||
|
|
||||||
|
Python 更依赖约定、属性和接口设计。单下划线、双下划线及 `property` 会在下一课结合 Java 访问控制集中讲解。
|
||||||
|
|
||||||
|
### 12.6 用多个同名方法实现重载
|
||||||
|
|
||||||
|
Python 类体中后定义的同名方法会覆盖前一个定义,不能照搬 Java 重载。常见替代方式包括默认参数、关键字参数和单分派,后续按需讲解。
|
||||||
|
|
||||||
|
## 十三、课堂练习
|
||||||
|
|
||||||
|
打开 `practice.py`,完成一个 `Book` 类。练习覆盖:
|
||||||
|
|
||||||
|
1. `__init__` 和实例属性;
|
||||||
|
2. `self` 和实例方法;
|
||||||
|
3. 类属性;
|
||||||
|
4. `@classmethod` 命名工厂;
|
||||||
|
5. `@staticmethod` 校验方法;
|
||||||
|
6. 不使用 `new` 创建并运行对象。
|
||||||
|
|
||||||
|
每部分都附有 Java 对照提醒和最终预期输出,因此可以直接从练习开始。
|
||||||
|
|
||||||
|
## 十四、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入 `practice.py`,避免直接照抄。完成后可以把代码交给我,我会从以下三方面反馈:
|
||||||
|
|
||||||
|
1. 正确性:对象创建、属性和方法结果是否正确;
|
||||||
|
2. 可读性:命名、结构和注释是否符合 Python 习惯;
|
||||||
|
3. 知识掌握:是否仍残留 Java 式写法,能否解释选择原因。
|
||||||
|
|
||||||
|
## 十五、本课小结
|
||||||
|
|
||||||
|
- Python 用 `class` 定义类,但通常不写 `public`;
|
||||||
|
- 使用 `__init__` 初始化实例,创建对象时不写 `new`;
|
||||||
|
- `self` 类似 Java `this`,在定义实例方法时必须显式声明;
|
||||||
|
- 实例属性通常通过 `self.xxx` 在 `__init__` 中建立;
|
||||||
|
- 类属性类似 `static` 字段,但实例同名赋值会产生遮蔽;
|
||||||
|
- `@classmethod` 接收 `cls`,适合可继承的命名工厂;
|
||||||
|
- `@staticmethod` 不接收实例或类,只处理与类有关的独立逻辑;
|
||||||
|
- Python 更动态,但成熟项目仍需要稳定、清晰的对象结构。
|
||||||
|
|
||||||
|
## 十六、验收标准
|
||||||
|
|
||||||
|
- 能不使用 `new` 创建 Python 对象;
|
||||||
|
- 能正确编写 `__init__` 和 `self`;
|
||||||
|
- 能解释 `self` 与 Java `this` 的语法差异;
|
||||||
|
- 能区分实例属性和类属性;
|
||||||
|
- 能区分实例方法、类方法和静态方法;
|
||||||
|
- 能解释为什么命名工厂中优先使用 `cls(...)`;
|
||||||
|
- 能指出 Python 不支持 Java 式同名方法重载;
|
||||||
|
- `practice.py` 的实际输出与题目预期一致。
|
||||||
63
03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py
Normal file
63
03_面向对象/3_1_Python与Java的类和对象/class_object_comparison.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 第 3-1 课完整示例:Python 与 Java 的类和对象
|
||||||
|
#
|
||||||
|
# 本示例不重复讲解通用的面向对象概念,而是集中展示 Python 与 Java 的写法差异。
|
||||||
|
# 运行程序时,每一段输出都会再次提醒对应差异,便于只看代码和结果也能掌握重点。
|
||||||
|
|
||||||
|
|
||||||
|
class Agent:
|
||||||
|
"""表示一个简单的 Agent,并演示 Python 类的核心语法。"""
|
||||||
|
|
||||||
|
# Python 的类属性类似 Java 的 static 字段,由所有实例共同访问。
|
||||||
|
platform = "Codex"
|
||||||
|
|
||||||
|
def __init__(self, name: str, model: str = "gpt-5") -> None:
|
||||||
|
"""初始化实例属性;__init__ 的作用接近 Java 构造器,但它不是类名同名方法。"""
|
||||||
|
# Python 通常直接在 __init__ 中创建实例属性,不需要提前声明字段。
|
||||||
|
# self 类似 Java 的 this,但在实例方法定义中必须显式写出来。
|
||||||
|
self.name = name
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def describe(self) -> str:
|
||||||
|
"""返回当前对象的说明文字。"""
|
||||||
|
return f"{self.name} 使用 {self.model},运行平台:{self.platform}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, config: dict[str, str]) -> "Agent":
|
||||||
|
"""通过配置创建对象;cls 指向当前类,作用类似可继承的命名工厂。"""
|
||||||
|
return cls(config["name"], config.get("model", "gpt-5"))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_valid_name(name: str) -> bool:
|
||||||
|
"""校验名称;该方法不需要访问某个对象或当前类。"""
|
||||||
|
return bool(name.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""创建对象并输出 Python 与 Java 的关键差异。"""
|
||||||
|
code_agent = Agent("代码助手", "gpt-5.2")
|
||||||
|
review_agent = Agent.from_config({"name": "审查助手"})
|
||||||
|
|
||||||
|
print("差异 1:Python 使用 __init__ 初始化对象,不写与类同名的构造器。")
|
||||||
|
print(code_agent.describe())
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 2:self 类似 Java 的 this,但定义实例方法时必须显式声明。")
|
||||||
|
print(f"通过对象访问实例属性:{code_agent.name}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 3:实例属性通常在运行时创建,不需要像 Java 字段那样预先声明。")
|
||||||
|
code_agent.status = "工作中"
|
||||||
|
print(f"动态添加的 status:{code_agent.status}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 4:类属性类似 static 字段,但也能通过实例读取。")
|
||||||
|
print(f"通过类读取:{Agent.platform};通过实例读取:{review_agent.platform}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 5:Python 不使用 new,直接调用类即可创建对象。")
|
||||||
|
print(review_agent.describe())
|
||||||
|
print(f"名称是否合法:{Agent.is_valid_name(review_agent.name)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
82
03_面向对象/3_1_Python与Java的类和对象/practice.py
Normal file
82
03_面向对象/3_1_Python与Java的类和对象/practice.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# 第 3-1 课课堂练习:Python 与 Java 的类和对象
|
||||||
|
#
|
||||||
|
# 这份练习把关键差异直接写进题目,不要求先完整阅读讲义。
|
||||||
|
# 请先独立完成;不要删除题目、预期输出和自查说明。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:定义 Book 类
|
||||||
|
# 1. 不要像 Java 一样预先声明 title、author、price 字段。
|
||||||
|
# 2. 定义 __init__(self, title, author, price=0.0) 方法。
|
||||||
|
# 3. 为 title 和 author 添加 str 类型注解,为 price 添加 float 类型注解。
|
||||||
|
# 4. 返回值标注为 None。
|
||||||
|
# 5. 在 __init__ 中通过 self.title、self.author、self.price 创建实例属性。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - Python 的 __init__ 承担初始化职责,但不是“与类同名的构造器”;
|
||||||
|
# - self 类似 Java 的 this,但必须写在方法参数列表中;
|
||||||
|
# - 创建对象时不写 new,也不手动传入 self。
|
||||||
|
|
||||||
|
|
||||||
|
# 第二部分:定义实例方法 get_summary(self)
|
||||||
|
# 1. 返回值标注为 str。
|
||||||
|
# 2. 返回格式:书名:《Python 后端开发》|作者:小明|价格:59.0 元。
|
||||||
|
# 3. 必须通过 self 读取三个实例属性。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - Python 方法没有 public 关键字;
|
||||||
|
# - 调用时写 book.get_summary(),Python 会自动把 book 传给 self。
|
||||||
|
|
||||||
|
# 第三部分:添加类属性 category
|
||||||
|
# 1. 在 Book 类中、所有方法外定义 category = "编程"。
|
||||||
|
# 2. 在后面的 main() 中分别使用 Book.category 和 book.category 输出它。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - 类属性在用途上类似 Java 的 static 字段;
|
||||||
|
# - 为避免实例属性遮蔽类属性,本题不要执行 book.category = ...。
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:定义类方法 from_text(cls, text)
|
||||||
|
# 1. 在方法上方添加 @classmethod。
|
||||||
|
# 2. text 的格式固定为“书名|作者”,例如“流畅的 Python|Luciano”。
|
||||||
|
# 3. 使用 text.split("|") 得到书名和作者。
|
||||||
|
# 4. 使用 cls(title, author) 创建并返回对象,让 price 使用默认值。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - cls 指向当前类,classmethod 常用于实现命名工厂;
|
||||||
|
# - 不要把类名 Book 写死在 return 中,使用 cls 更利于继承。
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:定义静态方法 is_valid_price(price)
|
||||||
|
# 1. 在方法上方添加 @staticmethod。
|
||||||
|
# 2. price 标注为 float,返回值标注为 bool。
|
||||||
|
# 3. price 大于或等于 0 时返回 True,否则返回 False。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - staticmethod 不接收 self 或 cls;
|
||||||
|
# - 当逻辑与 Book 有关、但不需要对象状态和类状态时,可以使用它。
|
||||||
|
|
||||||
|
|
||||||
|
# 第六部分:定义 main() 并完成运行验证
|
||||||
|
# 1. 使用 Book("Python 后端开发", "小明", 59.0) 创建 book,注意不写 new。
|
||||||
|
# 2. 输出 book.get_summary()。
|
||||||
|
# 3. 分别通过类和对象输出 category。
|
||||||
|
# 4. 使用 Book.from_text("流畅的 Python|Luciano") 创建 imported_book。
|
||||||
|
# 5. 输出 imported_book.get_summary(),其价格应为默认值 0.0。
|
||||||
|
# 6. 输出 Book.is_valid_price(59.0) 和 Book.is_valid_price(-1.0)。
|
||||||
|
# 7. 添加程序入口判断,直接运行本文件时调用 main()。
|
||||||
|
#
|
||||||
|
# 预期输出:
|
||||||
|
# 书名:《Python 后端开发》|作者:小明|价格:59.0 元
|
||||||
|
# 通过类读取分类:编程
|
||||||
|
# 通过对象读取分类:编程
|
||||||
|
# 书名:《流畅的 Python》|作者:Luciano|价格:0.0 元
|
||||||
|
# 59.0 是合法价格:True
|
||||||
|
# -1.0 是合法价格:False
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否能解释 self 与 Java this 的相同点和写法差异;
|
||||||
|
# 2. 是否记住 Python 创建对象时不写 new;
|
||||||
|
# 3. 是否知道 __init__ 负责初始化,而不是 Java 式的同名构造器;
|
||||||
|
# 4. 是否能区分实例属性、类属性、实例方法、类方法和静态方法;
|
||||||
|
# 5. 是否没有在类外直接替学习者写出答案。
|
||||||
244
03_面向对象/3_2_Python与Java的封装差异/README.md
Normal file
244
03_面向对象/3_2_Python与Java的封装差异/README.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# 第 3-2 课:Python 与 Java 的封装差异
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
Java 通过 `public`、`protected`、`private` 等访问修饰符实施较严格的访问控制。Python 更信任开发者,主要使用命名约定和属性(Property)表达接口边界。
|
||||||
|
|
||||||
|
本课不重复封装的一般理论,只讲 Java 开发者迁移到 Python 时必须调整的认识。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 理解 Python 没有与 Java 完全等价的强制 `private`;
|
||||||
|
2. 解释普通名称、单下划线和双下划线的区别;
|
||||||
|
3. 使用 `@property` 提供读取接口;
|
||||||
|
4. 使用 `@属性名.setter` 在赋值时执行校验;
|
||||||
|
5. 判断何时直接使用公开属性,何时需要 property;
|
||||||
|
6. 避免把 Java getter/setter 机械翻译成 Python 方法。
|
||||||
|
|
||||||
|
## 三、前置知识
|
||||||
|
|
||||||
|
- Java 的访问修饰符、getter 和 setter;
|
||||||
|
- Python 类、`self`、实例属性和实例方法;
|
||||||
|
- Python 异常处理。
|
||||||
|
|
||||||
|
## 四、差异速查表
|
||||||
|
|
||||||
|
| 意图 | Java 常见写法 | Python 常见写法 |
|
||||||
|
|---|---|---|
|
||||||
|
| 公开成员 | `public` | 普通名称,例如 `name` |
|
||||||
|
| 仅供内部使用 | `protected` 或包访问 | `_name` 命名约定 |
|
||||||
|
| 避免子类意外覆盖 | `private` | `__name` 触发名称改写 |
|
||||||
|
| 读取属性 | `getPrice()` | `product.price` 或 `@property` |
|
||||||
|
| 校验后赋值 | `setPrice(value)` | `product.price = value` 配合 setter |
|
||||||
|
| 强制安全边界 | 访问修饰符 | 下划线不能提供真正的安全隔离 |
|
||||||
|
|
||||||
|
## 五、单下划线:约定,不是限制
|
||||||
|
|
||||||
|
```python
|
||||||
|
self._owner = owner
|
||||||
|
```
|
||||||
|
|
||||||
|
`_owner` 表示“这是内部实现,外部代码通常不要直接依赖”。但是下面的代码仍然可以运行:
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(account._owner)
|
||||||
|
```
|
||||||
|
|
||||||
|
因此,单下划线主要是团队沟通约定,不是运行时访问控制。
|
||||||
|
|
||||||
|
## 六、双下划线:名称改写,不是绝对私有
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.__balance = 100.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 会进行名称改写(Name Mangling),把属性名称处理成类似:
|
||||||
|
|
||||||
|
```text
|
||||||
|
_Account__balance
|
||||||
|
```
|
||||||
|
|
||||||
|
主要目的之一是避免子类无意中使用相同名称覆盖父类属性。外部仍能通过改写后的名字访问它,所以它不是安全机制,也不能保护密码或密钥。
|
||||||
|
|
||||||
|
业务项目通常不要把所有内部属性都写成双下划线。只有确实需要避免子类名称冲突时再使用。
|
||||||
|
|
||||||
|
## 七、property:保持属性语法,同时执行方法逻辑
|
||||||
|
|
||||||
|
Java 常见写法:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public double getBalance() {
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBalance(double value) {
|
||||||
|
if (value < 0) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
balance = value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 可以写成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def balance(self) -> float:
|
||||||
|
return self.__balance
|
||||||
|
|
||||||
|
@balance.setter
|
||||||
|
def balance(self, value: float) -> None:
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("余额不能小于 0。")
|
||||||
|
self.__balance = value
|
||||||
|
```
|
||||||
|
|
||||||
|
调用方式仍像普通属性:
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(account.balance)
|
||||||
|
account.balance = 200.0
|
||||||
|
```
|
||||||
|
|
||||||
|
读取时实际执行 getter 方法,赋值时实际执行 setter 方法。
|
||||||
|
|
||||||
|
## 八、不要为每个字段机械创建 property
|
||||||
|
|
||||||
|
如果属性只是公开保存数据,没有校验、计算或兼容需求,可以直接使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.name = name
|
||||||
|
```
|
||||||
|
|
||||||
|
不需要照搬 Java,为它创建无逻辑的 `get_name()` 和 `set_name()`。
|
||||||
|
|
||||||
|
适合使用 property 的情况包括:
|
||||||
|
|
||||||
|
- 赋值时需要校验;
|
||||||
|
- 返回值需要动态计算;
|
||||||
|
- 希望提供只读接口;
|
||||||
|
- 内部存储方式可能改变,但不希望调用方式变化。
|
||||||
|
|
||||||
|
## 九、只读 property
|
||||||
|
|
||||||
|
只定义 getter、不定义 setter:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@property
|
||||||
|
def owner(self) -> str:
|
||||||
|
return self._owner
|
||||||
|
```
|
||||||
|
|
||||||
|
调用者可以读取 `account.owner`,但执行 `account.owner = "其他人"` 时会报错。
|
||||||
|
|
||||||
|
需要注意:这仍然不是不可绕过的安全边界。调用者如果直接修改 `_owner`,Python 不会阻止。它表达的是正常接口,不是对恶意代码的防护。
|
||||||
|
|
||||||
|
## 十、初始化时复用 setter
|
||||||
|
|
||||||
|
下面的写法可以让初始值和后续赋值使用相同校验:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def __init__(self, balance: float) -> None:
|
||||||
|
self.__balance = 0.0
|
||||||
|
self.balance = balance
|
||||||
|
```
|
||||||
|
|
||||||
|
第二行 `self.balance = balance` 会调用 property setter。这样不会出现“对象创建时允许负数,创建后却不允许负数”的规则不一致。
|
||||||
|
|
||||||
|
## 十一、完整示例
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py
|
||||||
|
```
|
||||||
|
|
||||||
|
运行命令:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\03_面向对象\3_2_Python与Java的封装差异\encapsulation_comparison.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例输出会直接展示:
|
||||||
|
|
||||||
|
1. 单下划线属性仍能从外部访问;
|
||||||
|
2. property 使用属性语法执行校验逻辑;
|
||||||
|
3. 双下划线在对象中实际发生了名称改写;
|
||||||
|
4. 非法余额会被 setter 阻止。
|
||||||
|
|
||||||
|
## 十二、常见误区
|
||||||
|
|
||||||
|
### 12.1 认为 `_name` 等于 Java protected
|
||||||
|
|
||||||
|
它只是一种命名约定,不会限制外部或非子类代码访问。
|
||||||
|
|
||||||
|
### 12.2 认为 `__name` 能保护敏感信息
|
||||||
|
|
||||||
|
双下划线不是加密或权限控制。敏感信息仍需使用安全的配置、存储和权限机制。
|
||||||
|
|
||||||
|
### 12.3 为所有属性生成 getter 和 setter
|
||||||
|
|
||||||
|
没有额外逻辑时,直接访问公开属性更符合 Python 风格。需要保持属性语法并增加逻辑时再使用 property。
|
||||||
|
|
||||||
|
### 12.4 在 setter 内给 property 本身赋值
|
||||||
|
|
||||||
|
下面会无限递归:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@balance.setter
|
||||||
|
def balance(self, value: float) -> None:
|
||||||
|
self.balance = value
|
||||||
|
```
|
||||||
|
|
||||||
|
setter 内应给实际存储属性赋值,例如 `self.__balance = value`。
|
||||||
|
|
||||||
|
## 十三、课堂练习
|
||||||
|
|
||||||
|
打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_2_Python与Java的封装差异/practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成 `Product` 类,练习内容包括:
|
||||||
|
|
||||||
|
1. 单下划线和双下划线属性;
|
||||||
|
2. 只读 `name` property;
|
||||||
|
3. 可读写 `price` property;
|
||||||
|
4. 价格与折扣率校验;
|
||||||
|
5. 使用 `try...except` 验证非法数据。
|
||||||
|
|
||||||
|
练习题中包含 Java 对照提醒和完整预期输出,可以直接从练习开始。
|
||||||
|
|
||||||
|
## 十四、参考答案
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。完成后,我会按照以下层级验证:
|
||||||
|
|
||||||
|
- 必须修复:语法错误、运行错误、业务结果错误、关键知识点使用错误;
|
||||||
|
- 建议改进:重复逻辑、容易产生误解的结构、明显影响可读性的写法;
|
||||||
|
- 可选优化:非必要类型注解、输出美化和不影响理解的风格统一。
|
||||||
|
|
||||||
|
只有“必须修复”的问题会阻止课程验收。
|
||||||
|
|
||||||
|
## 十五、本课小结
|
||||||
|
|
||||||
|
- Python 主要通过约定和接口设计实现封装;
|
||||||
|
- `_name` 表示内部使用,但外部仍能访问;
|
||||||
|
- `__name` 触发名称改写,主要用于避免子类名称冲突;
|
||||||
|
- 双下划线不是 Java `private` 的完全等价物,也不是安全机制;
|
||||||
|
- `@property` 让方法逻辑保留属性式调用体验;
|
||||||
|
- setter 可以集中完成赋值校验;
|
||||||
|
- 没有额外逻辑时,不必机械创建 getter 和 setter。
|
||||||
|
|
||||||
|
## 十六、验收标准
|
||||||
|
|
||||||
|
- 能解释单下划线和双下划线的区别;
|
||||||
|
- 能说明下划线为什么不能保护敏感信息;
|
||||||
|
- 能定义只读 property;
|
||||||
|
- 能定义包含校验的 property setter;
|
||||||
|
- 能避免 setter 自我赋值造成无限递归;
|
||||||
|
- 能用 property 统一初始化和后续赋值规则;
|
||||||
|
- `practice.py` 能正确阻止负数价格;
|
||||||
|
- 能说明 Python property 与 Java getter/setter 的调用差异。
|
||||||
69
03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py
Normal file
69
03_面向对象/3_2_Python与Java的封装差异/encapsulation_comparison.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 第 3-2 课完整示例:Python 与 Java 的封装差异
|
||||||
|
#
|
||||||
|
# 本示例重点演示 Python 的下划线命名约定和 @property。
|
||||||
|
# 运行时会直接输出关键结论,不完整阅读讲义也能看到本课重点。
|
||||||
|
|
||||||
|
|
||||||
|
class Account:
|
||||||
|
"""表示一个账户,并演示 Python 常见的封装方式。"""
|
||||||
|
|
||||||
|
def __init__(self, owner: str, balance: float = 0.0) -> None:
|
||||||
|
# 单下划线表示“仅供内部使用”的约定,但不会禁止外部访问。
|
||||||
|
self._owner = owner
|
||||||
|
|
||||||
|
# 双下划线会触发名称改写(Name Mangling),避免子类意外覆盖同名属性。
|
||||||
|
# 它不是 Java private 那样的安全边界,也不能用于保护敏感信息。
|
||||||
|
self.__balance = 0.0
|
||||||
|
self.balance = balance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self) -> str:
|
||||||
|
"""允许调用者以 account.owner 的形式读取账户所有者。"""
|
||||||
|
return self._owner
|
||||||
|
|
||||||
|
@property
|
||||||
|
def balance(self) -> float:
|
||||||
|
"""读取余额;调用时不需要写成 get_balance()。"""
|
||||||
|
return self.__balance
|
||||||
|
|
||||||
|
@balance.setter
|
||||||
|
def balance(self, value: float) -> None:
|
||||||
|
"""设置余额,并集中执行非负校验。"""
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("余额不能小于 0。")
|
||||||
|
self.__balance = value
|
||||||
|
|
||||||
|
def deposit(self, amount: float) -> None:
|
||||||
|
"""存入资金,并复用 balance 属性中的校验入口。"""
|
||||||
|
if amount <= 0:
|
||||||
|
raise ValueError("存入金额必须大于 0。")
|
||||||
|
self.balance = self.balance + amount
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""运行封装差异示例。"""
|
||||||
|
account = Account("小明", 100.0)
|
||||||
|
|
||||||
|
print("差异 1:Python 主要依靠命名约定表达成员用途。")
|
||||||
|
print(f"单下划线属性仍可访问:{account._owner}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 2:@property 让方法校验和普通属性访问可以同时存在。")
|
||||||
|
print(f"{account.owner} 当前余额:{account.balance}")
|
||||||
|
account.balance = 150.0
|
||||||
|
print(f"赋值后的余额:{account.balance}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 3:双下划线是名称改写,不是绝对私有或安全机制。")
|
||||||
|
print(f"对象中保存的实际属性名:{account.__dict__}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 4:属性赋值可以统一执行校验。")
|
||||||
|
try:
|
||||||
|
account.balance = -1.0
|
||||||
|
except ValueError as error:
|
||||||
|
print(f"非法赋值已被阻止:{error}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
70
03_面向对象/3_2_Python与Java的封装差异/practice.py
Normal file
70
03_面向对象/3_2_Python与Java的封装差异/practice.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# 第 3-2 课课堂练习:Python 与 Java 的封装差异
|
||||||
|
#
|
||||||
|
# 关键规则已经写进每部分题目,可以不先完整阅读讲义。
|
||||||
|
# 请保留题目、预期结果和自查说明,并独立完成 Product 类。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:定义 Product 类和初始化方法
|
||||||
|
# 1. 定义 __init__(self, name, price) 方法。
|
||||||
|
# 2. name 标注为 str,price 标注为 float,返回值标注可自行决定是否添加。
|
||||||
|
# 3. 使用 self._name = name 保存名称。
|
||||||
|
# 4. 使用 self.__price = 0.0 建立双下划线属性。
|
||||||
|
# 5. 最后执行 self.price = price,通过后面定义的 setter 完成初始价格校验。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - _name 只表示“内部使用”的约定,外部访问不会产生语法错误;
|
||||||
|
# - __price 会触发名称改写,但不等于 Java 的强制 private;
|
||||||
|
# - 双下划线不能作为保存密码等敏感信息的安全措施。
|
||||||
|
|
||||||
|
# 第二部分:为 name 定义只读 property
|
||||||
|
# 1. 在方法上方添加 @property。
|
||||||
|
# 2. 定义 name(self) 方法并返回 self._name。
|
||||||
|
# 3. 不要定义 @name.setter,使调用者不能通过 product.name = ... 正常修改名称。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - Java 常写 getName();Python 调用时直接写 product.name;
|
||||||
|
# - 虽然调用形式像字段,但实际上会执行 name() 方法。
|
||||||
|
|
||||||
|
|
||||||
|
# 第三部分:为 price 定义可读写 property
|
||||||
|
# 1. 使用 @property 定义 price(self),返回 self.__price。
|
||||||
|
# 2. 使用 @price.setter 定义同名方法 price(self, value)。
|
||||||
|
# 3. value 标注为 float。
|
||||||
|
# 4. value 小于 0 时执行 raise ValueError("价格不能小于 0。")。
|
||||||
|
# 5. 校验通过后执行 self.__price = value。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - 这相当于 getPrice() 和 setPrice(),但调用形式仍是 product.price;
|
||||||
|
# - 不要同时保留公开的 price 实例属性,否则会和 property 的职责混淆。
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:定义 discount(self, rate) 实例方法
|
||||||
|
# 1. rate 标注为 float。
|
||||||
|
# 2. rate 必须满足 0 <= rate <= 1,否则抛出 ValueError("折扣率必须在 0 到 1 之间。")。
|
||||||
|
# 3. 合法时执行 self.price = self.price * rate。
|
||||||
|
# 4. 通过 property 修改价格,复用统一的价格入口。
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:定义 main() 并验证行为
|
||||||
|
# 1. 创建 Product("机械键盘", 500.0)。
|
||||||
|
# 2. 输出“商品:机械键盘|价格:500.0 元”。
|
||||||
|
# 3. 执行 product.price = 450.0,再输出“修改后价格:450.0 元”。
|
||||||
|
# 4. 执行 product.discount(0.8),再输出“折扣后价格:360.0 元”。
|
||||||
|
# 5. 使用 try...except 测试 product.price = -1.0。
|
||||||
|
# 6. 捕获 ValueError 后输出“非法价格已被阻止:价格不能小于 0。”。
|
||||||
|
# 7. 添加程序入口判断并调用 main()。
|
||||||
|
#
|
||||||
|
# 预期输出:
|
||||||
|
# 商品:机械键盘|价格:500.0 元
|
||||||
|
# 修改后价格:450.0 元
|
||||||
|
# 折扣后价格:360.0 元
|
||||||
|
# 非法价格已被阻止:价格不能小于 0。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否知道单下划线只是内部使用约定;
|
||||||
|
# 2. 是否知道双下划线会名称改写,但不是绝对私有;
|
||||||
|
# 3. 是否能用 @property 代替简单的 Java getter;
|
||||||
|
# 4. 是否能用 @属性名.setter 在赋值时执行校验;
|
||||||
|
# 5. 是否能说明 product.price 看似访问字段、实际会执行方法;
|
||||||
|
# 6. 是否只把运行错误和逻辑错误视为阻塞项,不把可选类型注解当作必须项。
|
||||||
242
03_面向对象/3_3_Python与Java的继承和多态差异/README.md
Normal file
242
03_面向对象/3_3_Python与Java的继承和多态差异/README.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# 第 3-3 课:Python 与 Java 的继承和多态差异
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
你已经掌握 Java 的继承、接口和多态,因此本课不重复通用理论,重点建立以下 Python 差异:
|
||||||
|
|
||||||
|
- `super()` 不需要显式写父类名;
|
||||||
|
- Python 没有强制的 `@Override`;
|
||||||
|
- Python 支持多继承,并使用方法解析顺序决定查找路径;
|
||||||
|
- 多态调用经常基于鸭子类型,不要求对象继承共同父类或显式实现接口。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 定义父类和子类并重写方法;
|
||||||
|
2. 使用 `super()` 复用继承链中的实现;
|
||||||
|
3. 解释方法解析顺序(Method Resolution Order,MRO);
|
||||||
|
4. 使用鸭子类型(Duck Typing)完成统一调用;
|
||||||
|
5. 理解 Python 多继承和 Java 单类继承的差异;
|
||||||
|
6. 避免通过大量 `isinstance()` 分支模拟多态。
|
||||||
|
|
||||||
|
## 三、差异速查表
|
||||||
|
|
||||||
|
| 关注点 | Java | Python |
|
||||||
|
|---|---|---|
|
||||||
|
| 继承类 | `extends` | `class Child(Parent):` |
|
||||||
|
| 调用继承逻辑 | `super(...)` | 常用 `super()` |
|
||||||
|
| 重写标记 | 推荐 `@Override` | 没有强制对应标记 |
|
||||||
|
| 类继承数量 | 单继承 | 支持多继承 |
|
||||||
|
| 方法查找 | 类继承链 | 按 MRO 查找 |
|
||||||
|
| 接口多态 | 通常显式 `implements` | 常见鸭子类型,无需声明接口 |
|
||||||
|
| 类型判断 | 编译期和运行时类型体系 | 运行时更关注对象是否提供所需行为 |
|
||||||
|
|
||||||
|
## 四、继承和方法重写
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Notifier:
|
||||||
|
def send(self, message: str) -> None:
|
||||||
|
raise NotImplementedError("子类必须实现 send() 方法。")
|
||||||
|
|
||||||
|
|
||||||
|
class EmailNotifier(Notifier):
|
||||||
|
def send(self, message: str) -> None:
|
||||||
|
print(f"发送邮件:{message}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 子类把父类写在类名后的圆括号中,不使用 `extends`。子类定义同名方法即可重写,语言本身不要求添加 `@Override`。
|
||||||
|
|
||||||
|
缺少 `@Override` 也意味着方法名拼错时不会获得同样的编译期保护,因此测试和静态检查更加重要。
|
||||||
|
|
||||||
|
## 五、正确理解 super()
|
||||||
|
|
||||||
|
```python
|
||||||
|
class EmailNotifier(Notifier):
|
||||||
|
def __init__(self, sender: str, address: str) -> None:
|
||||||
|
super().__init__(sender)
|
||||||
|
self.address = address
|
||||||
|
```
|
||||||
|
|
||||||
|
Python 3 通常直接写 `super()`,不需要传入当前类和 `self`。
|
||||||
|
|
||||||
|
在简单单继承中,可以暂时把它理解为调用父类实现。但更准确的理解是:`super()` 会沿当前类的方法解析顺序继续查找方法。这个区别在多继承中尤其重要。
|
||||||
|
|
||||||
|
不要写死父类名称:
|
||||||
|
|
||||||
|
```python
|
||||||
|
Notifier.__init__(self, sender)
|
||||||
|
```
|
||||||
|
|
||||||
|
写死类名可能破坏多继承中的协作调用链。
|
||||||
|
|
||||||
|
## 六、方法解析顺序 MRO
|
||||||
|
|
||||||
|
Python 支持一个类继承多个父类:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LoggedEmailNotifier(EmailNotifier, LogMixin):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
可以查看查找顺序:
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(LoggedEmailNotifier.mro())
|
||||||
|
```
|
||||||
|
|
||||||
|
当调用某个方法时,Python 会按 MRO 中的顺序寻找第一个匹配实现。日常代码应避免设计过于复杂的多继承层次。
|
||||||
|
|
||||||
|
常见的安全用途是混入类(Mixin):它只提供一项小而明确的可复用能力,通常不独立代表完整业务对象。
|
||||||
|
|
||||||
|
## 七、鸭子类型
|
||||||
|
|
||||||
|
经典描述是:“如果一个对象走起来像鸭子、叫起来像鸭子,就把它当作鸭子使用。”
|
||||||
|
|
||||||
|
在代码中,这表示调用者更关心对象是否提供所需行为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def send_message(notifier, message: str) -> None:
|
||||||
|
notifier.send(message)
|
||||||
|
```
|
||||||
|
|
||||||
|
下面两个类不必拥有共同父类:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class EmailNotifier:
|
||||||
|
def send(self, message: str) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleNotifier:
|
||||||
|
def send(self, message: str) -> None:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
只要对象具有可调用的 `send()` 方法,`send_message()` 就可以使用它。
|
||||||
|
|
||||||
|
与 Java 接口多态相比:
|
||||||
|
|
||||||
|
- Java 常在编译期要求对象声明实现某个接口;
|
||||||
|
- Python 运行时可以直接尝试调用所需方法;
|
||||||
|
- Python 也能使用协议(Protocol)为鸭子类型提供静态检查,后续结合类型设计再展开。
|
||||||
|
|
||||||
|
## 八、不要用类型分支代替多态
|
||||||
|
|
||||||
|
不推荐:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if isinstance(processor, CardProcessor):
|
||||||
|
...
|
||||||
|
elif isinstance(processor, QrCodeProcessor):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐让每个对象自己实现统一行为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
processor.pay(amount)
|
||||||
|
```
|
||||||
|
|
||||||
|
这样新增处理器时,调用方通常不需要修改。
|
||||||
|
|
||||||
|
`isinstance()` 本身并非错误;当业务规则确实依赖类型时可以使用。但如果只是为了选择同一操作的不同实现,通常应优先使用多态。
|
||||||
|
|
||||||
|
## 九、继承还是组合
|
||||||
|
|
||||||
|
Java 与 Python 都不应为了复用少量代码而滥用继承。
|
||||||
|
|
||||||
|
- 真正存在“是一个”关系,并需要替换父类型行为时,可以考虑继承;
|
||||||
|
- 只是“拥有一个”协作者或需要灵活替换能力时,优先考虑组合;
|
||||||
|
- 只需要调用统一行为时,Python 的鸭子类型可能已经足够。
|
||||||
|
|
||||||
|
组合会在后续业务模型课程中结合实例继续练习。
|
||||||
|
|
||||||
|
## 十、完整示例和运行方法
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_3_Python与Java的继承和多态差异/inheritance_polymorphism_comparison.py
|
||||||
|
```
|
||||||
|
|
||||||
|
在项目根目录运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\03_面向对象\3_3_Python与Java的继承和多态差异\inheritance_polymorphism_comparison.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例输出会直接展示:
|
||||||
|
|
||||||
|
1. `super()` 复用继承逻辑;
|
||||||
|
2. 方法重写;
|
||||||
|
3. 没有共同父类的鸭子类型调用;
|
||||||
|
4. 多继承类的 MRO。
|
||||||
|
|
||||||
|
## 十一、常见错误
|
||||||
|
|
||||||
|
### 11.1 把 super() 当成固定父类对象
|
||||||
|
|
||||||
|
它实际按照 MRO 继续查找。在多继承中,下一站不一定是你凭直觉认定的某个父类。
|
||||||
|
|
||||||
|
### 11.2 忘记调用父类初始化逻辑
|
||||||
|
|
||||||
|
如果父类负责建立必要属性,子类通常需要调用 `super().__init__(...)`,否则使用父类属性时可能出现 `AttributeError`。
|
||||||
|
|
||||||
|
### 11.3 认为多态必须继承共同父类
|
||||||
|
|
||||||
|
Python 鸭子类型只要求对象提供当前操作需要的方法。
|
||||||
|
|
||||||
|
### 11.4 滥用多继承
|
||||||
|
|
||||||
|
多继承会增加 MRO 和初始化协作的理解成本。优先使用清晰的小型 Mixin,复杂业务能力通常更适合组合。
|
||||||
|
|
||||||
|
### 11.5 假设方法已经被正确重写
|
||||||
|
|
||||||
|
Python 没有强制 `@Override`。方法名或参数写错时,应通过测试、类型检查或代码审查发现。
|
||||||
|
|
||||||
|
## 十二、课堂练习
|
||||||
|
|
||||||
|
打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_3_Python与Java的继承和多态差异/practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成支付处理器练习:
|
||||||
|
|
||||||
|
1. 定义父类统一操作;
|
||||||
|
2. 子类通过 `super()` 复用初始化;
|
||||||
|
3. 未继承父类的二维码处理器参与统一调用;
|
||||||
|
4. 使用一个函数调用两种处理器;
|
||||||
|
5. 输出并观察 `CardProcessor.mro()`。
|
||||||
|
|
||||||
|
## 十三、参考答案与验证标准
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。完成后按以下层级验证:
|
||||||
|
|
||||||
|
- 必须修复:语法错误、运行错误、支付结果错误、没有使用 `super()`、通过具体类型分支实现调用;
|
||||||
|
- 建议改进:重复代码、职责混乱、明显影响理解的命名;
|
||||||
|
- 可选优化:非必要类型注解、输出美化和不影响行为的格式问题。
|
||||||
|
|
||||||
|
只有“必须修复”会阻止课程验收。
|
||||||
|
|
||||||
|
## 十四、本课小结
|
||||||
|
|
||||||
|
- Python 使用 `class Child(Parent)` 表示继承;
|
||||||
|
- 子类定义同名方法即可重写,没有强制 `@Override`;
|
||||||
|
- `super()` 会沿 MRO 继续查找实现;
|
||||||
|
- Python 支持多继承,但复杂多继承应谨慎使用;
|
||||||
|
- 鸭子类型关注对象提供的行为,不要求共同父类或显式接口;
|
||||||
|
- 统一调用不应依赖大量具体类型判断;
|
||||||
|
- 继承用于真正的类型关系,单纯复用通常优先考虑组合。
|
||||||
|
|
||||||
|
## 十五、验收标准
|
||||||
|
|
||||||
|
- 能正确使用 `super().__init__()`;
|
||||||
|
- 能重写父类方法;
|
||||||
|
- 能输出并解释简单类的 MRO;
|
||||||
|
- 能让未继承共同父类的对象参与统一调用;
|
||||||
|
- 能解释鸭子类型与 Java 接口多态的区别;
|
||||||
|
- 能避免通过 `isinstance()` 分支实现本题多态;
|
||||||
|
- `practice.py` 输出正确支付结果。
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 第 3-3 课完整示例:Python 与 Java 的继承和多态差异
|
||||||
|
#
|
||||||
|
# 示例集中演示 super()、方法重写、多继承的方法解析顺序和鸭子类型。
|
||||||
|
|
||||||
|
|
||||||
|
class Notifier:
|
||||||
|
"""所有通知器都可以复用的基础类。"""
|
||||||
|
|
||||||
|
def __init__(self, sender: str) -> None:
|
||||||
|
self.sender = sender
|
||||||
|
|
||||||
|
def send(self, message: str) -> None:
|
||||||
|
"""定义统一操作;具体子类负责实现发送行为。"""
|
||||||
|
raise NotImplementedError("子类必须实现 send() 方法。")
|
||||||
|
|
||||||
|
|
||||||
|
class EmailNotifier(Notifier):
|
||||||
|
"""通过继承实现邮件通知器。"""
|
||||||
|
|
||||||
|
def __init__(self, sender: str, address: str) -> None:
|
||||||
|
# Python 3 中通常直接写 super(),不需要传入类名和 self。
|
||||||
|
super().__init__(sender)
|
||||||
|
self.address = address
|
||||||
|
|
||||||
|
def send(self, message: str) -> None:
|
||||||
|
"""重写父类方法;Python 不要求添加 @Override。"""
|
||||||
|
print(f"邮件|{self.sender} -> {self.address}:{message}")
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleNotifier:
|
||||||
|
"""没有继承 Notifier,但同样提供 send() 方法。"""
|
||||||
|
|
||||||
|
def send(self, message: str) -> None:
|
||||||
|
print(f"控制台|{message}")
|
||||||
|
|
||||||
|
|
||||||
|
class LogMixin:
|
||||||
|
"""混入类(Mixin)只提供一项可复用能力。"""
|
||||||
|
|
||||||
|
def record(self) -> None:
|
||||||
|
print("通知行为已记录。")
|
||||||
|
|
||||||
|
|
||||||
|
class LoggedEmailNotifier(EmailNotifier, LogMixin):
|
||||||
|
"""Python 可以继承多个类,查找方法时遵循 MRO。"""
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(notifier, message: str) -> None:
|
||||||
|
"""只要传入对象具有 send() 方法,就可以完成调用。"""
|
||||||
|
notifier.send(message)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""运行继承和多态差异示例。"""
|
||||||
|
email = EmailNotifier("系统", "user@example.com")
|
||||||
|
console = ConsoleNotifier()
|
||||||
|
|
||||||
|
print("差异 1:子类使用 super() 复用父类逻辑,不需要写父类名称。")
|
||||||
|
print(f"邮件发送者:{email.sender}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 2:Python 重写方法时没有强制的 @Override。")
|
||||||
|
email.send("课程开始")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 3:鸭子类型关注对象有什么行为,而不是声明了什么接口。")
|
||||||
|
send_message(email, "继承得到 send()")
|
||||||
|
send_message(console, "未继承也能调用 send()")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 4:Python 支持多继承,并通过 MRO 决定方法查找顺序。")
|
||||||
|
logged_email = LoggedEmailNotifier("系统", "admin@example.com")
|
||||||
|
logged_email.record()
|
||||||
|
print([class_type.__name__ for class_type in LoggedEmailNotifier.mro()])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
72
03_面向对象/3_3_Python与Java的继承和多态差异/practice.py
Normal file
72
03_面向对象/3_3_Python与Java的继承和多态差异/practice.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# 第 3-3 课课堂练习:Python 与 Java 的继承和多态差异
|
||||||
|
#
|
||||||
|
# 关键差异已经写进每部分题目,可以直接完成练习。
|
||||||
|
# 请保留题目、预期结果和自查说明。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:定义 PaymentProcessor 父类
|
||||||
|
# 1. 定义 __init__(self, channel_name) 方法。
|
||||||
|
# 2. channel_name 标注为 str,并保存为 self.channel_name。
|
||||||
|
# 3. 定义 pay(self, amount) 方法,amount 标注为 float。
|
||||||
|
# 4. pay() 中执行 raise NotImplementedError("子类必须实现 pay() 方法。")。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - 本题暂不使用抽象基类(Abstract Base Class,ABC),先观察普通父类的写法;
|
||||||
|
# - Python 重写方法时不要求 @Override,拼错方法名也不会由该注解提醒。
|
||||||
|
|
||||||
|
|
||||||
|
# 第二部分:定义 CardProcessor 子类
|
||||||
|
# 1. 使用 class CardProcessor(PaymentProcessor): 表示继承。
|
||||||
|
# 2. 定义 __init__(self, card_number) 方法,card_number 标注为 str。
|
||||||
|
# 3. 执行 super().__init__("银行卡"),复用父类初始化逻辑。
|
||||||
|
# 4. 保存 self.card_number,只保留卡号最后四位即可:card_number[-4:]。
|
||||||
|
# 5. 重写 pay(self, amount),返回:
|
||||||
|
# “银行卡支付 100.0 元|卡号末四位:1234”。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - Python 3 通常直接写 super(),不需要传当前类和 self;
|
||||||
|
# - super() 按方法解析顺序继续查找方法,不应简单理解为固定的“父类对象”。
|
||||||
|
|
||||||
|
|
||||||
|
# 第三部分:定义 QrCodeProcessor 类,但不要继承 PaymentProcessor
|
||||||
|
# 1. 定义 pay(self, amount) 方法。
|
||||||
|
# 2. 返回“二维码支付 50.0 元”。
|
||||||
|
#
|
||||||
|
# 多态重点:
|
||||||
|
# - 该类没有 extends,也没有 implements;
|
||||||
|
# - 只要它提供调用者需要的 pay(),就能参与统一调用;
|
||||||
|
# - 这种“关注行为而非声明类型”的方式称为鸭子类型(Duck Typing)。
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:定义 process_payment(processor, amount) 函数
|
||||||
|
# 1. 调用 processor.pay(amount) 并 return 它的结果。
|
||||||
|
# 2. processor 的类型注解本题可以省略,不作为验收阻塞项。
|
||||||
|
# 3. 函数内部不要使用 isinstance() 判断具体类型。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - Java 通常要求 processor 实现统一接口;
|
||||||
|
# - Python 运行时只要求传入对象具有可调用的 pay() 方法。
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:定义 main() 并验证两种处理器
|
||||||
|
# 1. 创建 CardProcessor("6222020012341234")。
|
||||||
|
# 2. 创建 QrCodeProcessor()。
|
||||||
|
# 3. 使用 process_payment(card_processor, 100.0) 并输出结果。
|
||||||
|
# 4. 使用 process_payment(qr_processor, 50.0) 并输出结果。
|
||||||
|
# 5. 输出 CardProcessor 的方法解析顺序名称:
|
||||||
|
# [class_type.__name__ for class_type in CardProcessor.mro()]
|
||||||
|
# 6. 添加程序入口判断并调用 main()。
|
||||||
|
#
|
||||||
|
# 预期输出:
|
||||||
|
# 银行卡支付 100.0 元|卡号末四位:1234
|
||||||
|
# 二维码支付 50.0 元
|
||||||
|
# ['CardProcessor', 'PaymentProcessor', 'object']
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否会使用 super() 复用父类初始化;
|
||||||
|
# 2. 是否知道 Python 方法重写不要求 @Override;
|
||||||
|
# 3. 是否能解释 super() 与 MRO 的关系;
|
||||||
|
# 4. 是否知道未继承同一父类的对象也能参与鸭子类型调用;
|
||||||
|
# 5. 是否避免通过 isinstance() 分支实现伪多态;
|
||||||
|
# 6. 是否只把运行错误、结果错误和关键知识点错误视为阻塞项。
|
||||||
266
03_面向对象/3_4_Python特殊方法与数据类/README.md
Normal file
266
03_面向对象/3_4_Python特殊方法与数据类/README.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# 第 3-4 课:Python 特殊方法与数据类
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
Java 业务对象中经常需要构造器、`toString()`、`equals()`、`hashCode()`,项目也常借助 record 或 Lombok 减少样板代码。Python 通过特殊方法和数据类(Data Class,`@dataclass`)解决类似问题。
|
||||||
|
|
||||||
|
本课只选择业务开发中最常见的内容,不展开运算符重载的全部细节。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 理解 Python 特殊方法的调用机制;
|
||||||
|
2. 区分 `__str__` 和 `__repr__`;
|
||||||
|
3. 理解 `__eq__` 如何改变对象相等比较;
|
||||||
|
4. 使用 `@dataclass` 减少数据对象的样板代码;
|
||||||
|
5. 使用 `__post_init__` 执行业务校验;
|
||||||
|
6. 使用 `field(default_factory=list)` 避免共享可变默认值;
|
||||||
|
7. 理解 dataclass 与 Java POJO、record、Lombok 的边界差异。
|
||||||
|
|
||||||
|
## 三、差异速查表
|
||||||
|
|
||||||
|
| 需求 | Java 常见方式 | Python 常见方式 |
|
||||||
|
|---|---|---|
|
||||||
|
| 用户友好显示 | `toString()` | `__str__` |
|
||||||
|
| 开发调试表示 | 通常仍使用 `toString()` | `__repr__` |
|
||||||
|
| 内容相等 | 重写 `equals()` | 重写 `__eq__` |
|
||||||
|
| 减少数据类样板代码 | record 或 Lombok | `@dataclass` |
|
||||||
|
| 构造后校验 | 构造器代码块 | `__post_init__` |
|
||||||
|
| 每个对象独立列表 | 构造时 `new ArrayList<>()` | `field(default_factory=list)` |
|
||||||
|
|
||||||
|
## 四、特殊方法是什么
|
||||||
|
|
||||||
|
名称前后都有双下划线的方法称为特殊方法(Special Method),也常被称为双下方法(Dunder Method)。
|
||||||
|
|
||||||
|
通常不是直接调用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
item.__str__()
|
||||||
|
```
|
||||||
|
|
||||||
|
而是使用正常语法,让 Python 自动调用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
str(item)
|
||||||
|
print(item)
|
||||||
|
item_one == item_two
|
||||||
|
```
|
||||||
|
|
||||||
|
这与 Java 运行时在字符串拼接或打印对象时调用 `toString()` 的体验相似。
|
||||||
|
|
||||||
|
## 五、__str__ 与 __repr__
|
||||||
|
|
||||||
|
`__str__` 面向使用者,强调友好、易读:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}:{self.price} 元"
|
||||||
|
```
|
||||||
|
|
||||||
|
`__repr__` 面向开发和调试,通常包含类名和明确字段:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Product(name='机械键盘', price=500.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
调用关系:
|
||||||
|
|
||||||
|
- `str(obj)` 和 `print(obj)` 优先使用 `__str__`;
|
||||||
|
- `repr(obj)`、交互环境和容器显示通常使用 `__repr__`;
|
||||||
|
- 如果没有定义 `__str__`,Python 可能退回使用 `__repr__`。
|
||||||
|
|
||||||
|
## 六、__eq__ 与对象相等
|
||||||
|
|
||||||
|
普通类如果没有实现 `__eq__`,两个字段内容相同但身份不同的对象通常不相等:
|
||||||
|
|
||||||
|
```python
|
||||||
|
first = Product("键盘", 500.0)
|
||||||
|
second = Product("键盘", 500.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
实现 `__eq__` 后,可以按照业务字段比较。`@dataclass` 默认会根据声明的全部字段自动生成 `__eq__`。
|
||||||
|
|
||||||
|
注意:相等比较与对象身份判断不同:
|
||||||
|
|
||||||
|
- `first == second` 调用相等逻辑;
|
||||||
|
- `first is second` 判断是否为同一个对象。
|
||||||
|
|
||||||
|
## 七、@dataclass 生成什么
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Product:
|
||||||
|
name: str
|
||||||
|
price: float
|
||||||
|
```
|
||||||
|
|
||||||
|
默认会自动生成常用方法,包括:
|
||||||
|
|
||||||
|
- `__init__`:接收字段并初始化;
|
||||||
|
- `__repr__`:生成适合调试的表示;
|
||||||
|
- `__eq__`:按照字段值比较。
|
||||||
|
|
||||||
|
因此一般不要再手写完全相同的样板方法。
|
||||||
|
|
||||||
|
## 八、dataclass 不是什么
|
||||||
|
|
||||||
|
`@dataclass` 并不会:
|
||||||
|
|
||||||
|
- 自动进行运行时类型检查;
|
||||||
|
- 自动完成业务规则校验;
|
||||||
|
- 默认创建不可变对象;
|
||||||
|
- 自动等同于 Java record;
|
||||||
|
- 自动替代数据库对象关系映射(Object Relational Mapping,ORM)模型。
|
||||||
|
|
||||||
|
例如,`price: float` 只是类型提示,不能自动阻止负数或字符串。
|
||||||
|
|
||||||
|
## 九、使用 __post_init__ 校验
|
||||||
|
|
||||||
|
dataclass 自动生成 `__init__` 后会调用 `__post_init__`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class Product:
|
||||||
|
name: str
|
||||||
|
price: float
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.price < 0:
|
||||||
|
raise ValueError("价格不能小于 0。")
|
||||||
|
```
|
||||||
|
|
||||||
|
它适合执行字段间校验、格式整理或需要在初始化完成后执行的逻辑。
|
||||||
|
|
||||||
|
## 十、可变默认值与 default_factory
|
||||||
|
|
||||||
|
列表是可变对象。下面这种字段定义不应使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
items: list[Product] = []
|
||||||
|
```
|
||||||
|
|
||||||
|
dataclass 会直接拒绝常见的可变默认值,避免多个实例意外共享同一列表。
|
||||||
|
|
||||||
|
正确写法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cart:
|
||||||
|
items: list[Product] = field(default_factory=list)
|
||||||
|
```
|
||||||
|
|
||||||
|
`default_factory=list` 表示每次创建 `Cart` 时调用 `list()`,得到新的独立列表。
|
||||||
|
|
||||||
|
## 十一、与 Java record 和 Lombok 的区别
|
||||||
|
|
||||||
|
可以建立近似理解,但不要认为它们完全相同:
|
||||||
|
|
||||||
|
- dataclass 默认可变,Java record 的组件引用不可重新赋值;
|
||||||
|
- dataclass 可以写普通方法、继承和自定义特殊方法;
|
||||||
|
- dataclass 是 Python 标准库功能,不需要额外依赖或编译期注解处理;
|
||||||
|
- `@dataclass(frozen=True)` 可以限制字段重新赋值,但仍不是安全边界,也不是深层不可变。
|
||||||
|
|
||||||
|
本阶段先掌握默认 dataclass,`frozen=True` 按业务需要再使用。
|
||||||
|
|
||||||
|
## 十二、什么时候使用 dataclass
|
||||||
|
|
||||||
|
适合:
|
||||||
|
|
||||||
|
- 主要职责是保存数据;
|
||||||
|
- 需要清晰的字段定义;
|
||||||
|
- 希望自动获得初始化、调试显示和内容比较;
|
||||||
|
- 对象仍然包含少量与数据紧密相关的行为。
|
||||||
|
|
||||||
|
不应仅因为“这是一个类”就添加 dataclass。复杂服务对象、资源管理器或主要依靠行为工作的对象通常使用普通类更自然。
|
||||||
|
|
||||||
|
## 十三、完整示例与运行方法
|
||||||
|
|
||||||
|
示例文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_4_Python特殊方法与数据类/special_methods_dataclass_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
在项目根目录运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\03_面向对象\3_4_Python特殊方法与数据类\special_methods_dataclass_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例会展示普通类与 dataclass 的代码差异、字符串显示、字段相等、初始化校验和独立列表。
|
||||||
|
|
||||||
|
## 十四、常见错误
|
||||||
|
|
||||||
|
### 14.1 把 __repr__ 只当作另一个 __str__
|
||||||
|
|
||||||
|
二者用途不同:一个偏向开发调试,一个偏向使用者阅读。
|
||||||
|
|
||||||
|
### 14.2 认为类型注解会自动校验
|
||||||
|
|
||||||
|
`price: float` 不会自动拒绝字符串或负数。外部数据与业务规则仍需显式校验。
|
||||||
|
|
||||||
|
### 14.3 手写 dataclass 已生成的方法
|
||||||
|
|
||||||
|
如果自动行为已经符合需求,不要重复编写 `__init__`、`__repr__` 和 `__eq__`。
|
||||||
|
|
||||||
|
### 14.4 共享可变默认值
|
||||||
|
|
||||||
|
列表、字典和集合等可变默认值应使用 `default_factory` 创建。
|
||||||
|
|
||||||
|
### 14.5 默认认为 dataclass 可以哈希
|
||||||
|
|
||||||
|
可变 dataclass 通常不会自动提供可用的 `__hash__`。是否可作为字典键或集合元素涉及可变性和相等契约,本课不强行展开。
|
||||||
|
|
||||||
|
## 十五、课堂练习
|
||||||
|
|
||||||
|
打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_4_Python特殊方法与数据类/practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成订单项和购物车:
|
||||||
|
|
||||||
|
1. 使用 dataclass 定义字段;
|
||||||
|
2. 使用 `__post_init__` 校验单价和数量;
|
||||||
|
3. 使用 property 计算小计;
|
||||||
|
4. 使用 `__str__` 提供友好显示;
|
||||||
|
5. 验证自动生成的 `__repr__` 和 `__eq__`;
|
||||||
|
6. 使用 `default_factory=list` 创建独立购物车列表。
|
||||||
|
|
||||||
|
## 十六、参考答案与验证标准
|
||||||
|
|
||||||
|
参考答案暂不写入练习文件。完成后按以下层级验证:
|
||||||
|
|
||||||
|
- 必须修复:语法或运行错误、计算错误、校验缺失、共享列表、手写并破坏 dataclass 自动行为;
|
||||||
|
- 建议改进:重复计算、职责或命名明显不清晰;
|
||||||
|
- 可选优化:非必要类型注解、输出美化和不影响行为的格式问题。
|
||||||
|
|
||||||
|
只有“必须修复”会阻止课程验收。
|
||||||
|
|
||||||
|
## 十七、本课小结
|
||||||
|
|
||||||
|
- 特殊方法让对象参与 Python 的标准语法;
|
||||||
|
- `__str__` 面向使用者,`__repr__` 面向开发调试;
|
||||||
|
- `__eq__` 决定 `==` 的相等逻辑;
|
||||||
|
- `@dataclass` 自动生成初始化、调试表示和字段比较;
|
||||||
|
- dataclass 类型注解不会自动执行运行时校验;
|
||||||
|
- `__post_init__` 适合完成初始化后的业务校验;
|
||||||
|
- `default_factory` 为每个实例创建独立的可变字段。
|
||||||
|
|
||||||
|
## 十八、验收标准
|
||||||
|
|
||||||
|
- 能正确使用 `@dataclass`;
|
||||||
|
- 能解释 `__str__` 与 `__repr__` 的区别;
|
||||||
|
- 能观察自动生成的 `__eq__` 行为;
|
||||||
|
- 能使用 `__post_init__` 阻止非法数据;
|
||||||
|
- 能使用 `field(default_factory=list)`;
|
||||||
|
- 能说明 dataclass 与 Java record 并非完全等价;
|
||||||
|
- `practice.py` 的购物车计算与独立列表验证正确。
|
||||||
87
03_面向对象/3_4_Python特殊方法与数据类/practice.py
Normal file
87
03_面向对象/3_4_Python特殊方法与数据类/practice.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# 第 3-4 课课堂练习:Python 特殊方法与数据类
|
||||||
|
#
|
||||||
|
# 关键规则已经写入每部分题目,可以直接完成练习。
|
||||||
|
# 请保留题目、预期结果和自查说明。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:定义 OrderItem 数据类
|
||||||
|
# 1. 在类上方添加 @dataclass。
|
||||||
|
# 2. 定义三个字段,不需要手写 __init__:
|
||||||
|
# product_name: str
|
||||||
|
# unit_price: float
|
||||||
|
# quantity: int = 1
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - @dataclass 会生成 __init__、__repr__ 和 __eq__ 等常用方法;
|
||||||
|
# - 它类似减少 POJO 样板代码的工具,但不是 Java record 的完全等价物;
|
||||||
|
# - 数据类默认仍然可变,并不会自动进行运行时类型检查。
|
||||||
|
|
||||||
|
|
||||||
|
# 第二部分:使用 __post_init__ 校验字段
|
||||||
|
# 1. 定义 __post_init__(self) 方法。
|
||||||
|
# 2. unit_price 小于 0 时抛出 ValueError("单价不能小于 0。")。
|
||||||
|
# 3. quantity 小于或等于 0 时抛出 ValueError("数量必须大于 0。")。
|
||||||
|
#
|
||||||
|
# Java 对照提醒:
|
||||||
|
# - dataclass 自动生成 __init__ 后,会自动调用 __post_init__;
|
||||||
|
# - 类型注解只表达预期类型,不会阻止负数,所以业务校验仍需手写。
|
||||||
|
|
||||||
|
|
||||||
|
# 第三部分:定义 subtotal 只读 property
|
||||||
|
# 1. 使用 @property 定义 subtotal(self),返回单价乘数量。
|
||||||
|
# 2. 调用方式应为 item.subtotal,而不是 item.get_subtotal()。
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:定义 __str__(self)
|
||||||
|
# 1. 返回“机械键盘 × 2:1000.0 元”格式的使用者友好文字。
|
||||||
|
# 2. 使用 self.subtotal 取得小计。
|
||||||
|
#
|
||||||
|
# 特殊方法提醒:
|
||||||
|
# - print(item) 和 str(item) 会调用 __str__;
|
||||||
|
# - repr(item) 会使用 dataclass 自动生成的 __repr__;
|
||||||
|
# - item_one == item_two 会使用自动生成的 __eq__ 比较全部字段。
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:定义 ShoppingCart 数据类
|
||||||
|
# 1. 在类上方添加 @dataclass。
|
||||||
|
# 2. 定义 owner: str 字段。
|
||||||
|
# 3. 定义 items 字段:
|
||||||
|
# items: list[OrderItem] = field(default_factory=list)
|
||||||
|
# 4. 定义 add(self, item) 方法,把 item 添加到 self.items。
|
||||||
|
# 5. 定义 total 只读 property,返回所有 item.subtotal 的总和。
|
||||||
|
#
|
||||||
|
# 重要提醒:
|
||||||
|
# - 不要写 items = [] 作为类级默认列表;
|
||||||
|
# - default_factory=list 会为每个 ShoppingCart 对象创建独立列表;
|
||||||
|
# - 这与 Java 实例字段通常在构造时 new ArrayList<>() 的目的相似。
|
||||||
|
|
||||||
|
|
||||||
|
# 第六部分:定义 main() 并验证行为
|
||||||
|
# 1. 创建两个内容相同的 OrderItem("机械键盘", 500.0, 2)。
|
||||||
|
# 2. 输出第一个对象,预期调用 __str__。
|
||||||
|
# 3. 输出 repr(item_one),观察自动生成的调试表示。
|
||||||
|
# 4. 输出“两个订单项相等:True”。
|
||||||
|
# 5. 创建 ShoppingCart("小明") 和 ShoppingCart("小红")。
|
||||||
|
# 6. 只向小明的购物车添加第一个订单项。
|
||||||
|
# 7. 输出两个购物车的商品种类数,证明列表没有共享。
|
||||||
|
# 8. 输出小明购物车的总金额。
|
||||||
|
# 9. 使用 try...except 创建 OrderItem("错误商品", -1.0),捕获并输出错误。
|
||||||
|
# 10. 添加程序入口判断并调用 main()。
|
||||||
|
#
|
||||||
|
# 预期输出:
|
||||||
|
# 机械键盘 × 2:1000.0 元
|
||||||
|
# OrderItem(product_name='机械键盘', unit_price=500.0, quantity=2)
|
||||||
|
# 两个订单项相等:True
|
||||||
|
# 小明的商品种类数:1
|
||||||
|
# 小红的商品种类数:0
|
||||||
|
# 小明购物车总金额:1000.0 元
|
||||||
|
# 非法订单项已被阻止:单价不能小于 0。
|
||||||
|
|
||||||
|
|
||||||
|
# 完成后自查:
|
||||||
|
# 1. 是否没有为 OrderItem 手写 __init__、__repr__ 和 __eq__;
|
||||||
|
# 2. 是否能区分 __str__ 的用户显示与 __repr__ 的调试表示;
|
||||||
|
# 3. 是否知道 dataclass 的相等比较默认比较字段值;
|
||||||
|
# 4. 是否使用 __post_init__ 完成运行时业务校验;
|
||||||
|
# 5. 是否使用 default_factory=list 避免共享可变默认值;
|
||||||
|
# 6. 是否只把运行错误、结果错误和关键知识点错误视为阻塞项。
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# 第 3-4 课完整示例:Python 特殊方法与数据类
|
||||||
|
#
|
||||||
|
# 本示例对比普通类和 @dataclass,并演示 __str__、自动 __repr__、
|
||||||
|
# 自动 __eq__、__post_init__ 以及 field(default_factory=list)。
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
class ManualProduct:
|
||||||
|
"""手动实现初始化、字符串显示和相等比较的普通类。"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, price: float) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""返回适合开发和调试的对象表示。"""
|
||||||
|
return f"ManualProduct(name={self.name!r}, price={self.price!r})"
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
"""根据属性值判断两个商品是否相等。"""
|
||||||
|
if not isinstance(other, ManualProduct):
|
||||||
|
return NotImplemented
|
||||||
|
return self.name == other.name and self.price == other.price
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Product:
|
||||||
|
"""数据类会根据字段自动生成常用特殊方法。"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
price: float
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""在自动生成的 __init__ 执行后校验数据。"""
|
||||||
|
if self.price < 0:
|
||||||
|
raise ValueError("价格不能小于 0。")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""返回面向使用者的友好文字。"""
|
||||||
|
return f"{self.name}:{self.price} 元"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cart:
|
||||||
|
"""购物车使用工厂函数为每个实例创建独立列表。"""
|
||||||
|
|
||||||
|
owner: str
|
||||||
|
items: list[Product] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add(self, product: Product) -> None:
|
||||||
|
"""向当前购物车添加商品。"""
|
||||||
|
self.items.append(product)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""运行特殊方法与数据类示例。"""
|
||||||
|
manual_one = ManualProduct("机械键盘", 500.0)
|
||||||
|
manual_two = ManualProduct("机械键盘", 500.0)
|
||||||
|
|
||||||
|
print("差异 1:普通类需要手动实现 Java 常见样板能力。")
|
||||||
|
print(repr(manual_one))
|
||||||
|
print(f"两个普通类对象按内容相等:{manual_one == manual_two}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
keyboard_one = Product("机械键盘", 500.0)
|
||||||
|
keyboard_two = Product("机械键盘", 500.0)
|
||||||
|
|
||||||
|
print("差异 2:@dataclass 自动生成 __init__、__repr__ 和 __eq__。")
|
||||||
|
print(repr(keyboard_one))
|
||||||
|
print(f"两个数据类对象按字段相等:{keyboard_one == keyboard_two}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 3:__str__ 类似面向用户的 toString() 显示。")
|
||||||
|
print(str(keyboard_one))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 4:类型注解不会自动校验,业务规则要在 __post_init__ 中实现。")
|
||||||
|
try:
|
||||||
|
Product("错误商品", -1.0)
|
||||||
|
except ValueError as error:
|
||||||
|
print(f"非法商品已被阻止:{error}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("差异 5:default_factory 确保不同购物车使用不同列表。")
|
||||||
|
first_cart = Cart("小明")
|
||||||
|
second_cart = Cart("小红")
|
||||||
|
first_cart.add(keyboard_one)
|
||||||
|
print(f"小明的商品数:{len(first_cart.items)}")
|
||||||
|
print(f"小红的商品数:{len(second_cart.items)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
226
03_面向对象/3_5_面向对象综合项目/README.md
Normal file
226
03_面向对象/3_5_面向对象综合项目/README.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# 第 3-5 课:面向对象综合项目
|
||||||
|
|
||||||
|
## 一、项目目标
|
||||||
|
|
||||||
|
本课完成一个内存版图书管理系统,集中验证第三阶段知识。数据只保存在程序运行期间,不读写文件,也不连接数据库,避免重复第二阶段内容。
|
||||||
|
|
||||||
|
完成后,你将能够:
|
||||||
|
|
||||||
|
1. 从业务职责中识别类;
|
||||||
|
2. 使用 dataclass 定义数据对象;
|
||||||
|
3. 使用普通类组织业务操作;
|
||||||
|
4. 使用组合建立对象关系;
|
||||||
|
5. 使用 property 和特殊方法提供清晰接口;
|
||||||
|
6. 使用自定义异常表达业务失败;
|
||||||
|
7. 使用鸭子类型接入通知能力;
|
||||||
|
8. 对借书、还书和异常流程进行完整验证。
|
||||||
|
|
||||||
|
## 二、项目角色
|
||||||
|
|
||||||
|
| 角色 | 主要职责 |
|
||||||
|
|---|---|
|
||||||
|
| `Book` | 保存图书信息和可借状态,提供友好显示 |
|
||||||
|
| `Member` | 保存会员信息和已借 ISBN,计算已借数量 |
|
||||||
|
| `Library` | 管理图书、会员以及借还业务规则 |
|
||||||
|
| `ConsoleNotifier` | 输出借还成功通知 |
|
||||||
|
| 业务异常 | 区分图书不存在、会员不存在、图书不可借等失败 |
|
||||||
|
|
||||||
|
职责边界很重要:
|
||||||
|
|
||||||
|
- `Book` 不负责查找会员;
|
||||||
|
- `Member` 不负责维护整个图书馆;
|
||||||
|
- `ConsoleNotifier` 不负责改变借阅状态;
|
||||||
|
- `Library` 负责协调对象并保证借还规则一致。
|
||||||
|
|
||||||
|
## 三、为什么 Library 使用普通类
|
||||||
|
|
||||||
|
`Book` 和 `Member` 主要保存数据,适合使用 `@dataclass`。
|
||||||
|
|
||||||
|
`Library` 的主要职责是管理业务行为和对象关系,并不是单纯的数据容器,因此使用普通类更清晰。不是所有类都应该加 `@dataclass`。
|
||||||
|
|
||||||
|
## 四、组合关系
|
||||||
|
|
||||||
|
图书馆“拥有”图书和会员:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.books: dict[str, Book] = {}
|
||||||
|
self.members: dict[str, Member] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
这是组合,而不是继承。`Library` 不是一种 `Book`,所以不应写成:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Library(Book):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
也不应为了复用字典操作就让 `Library` 继承 `dict`。把字典作为内部属性更容易维护业务约束。
|
||||||
|
|
||||||
|
## 五、业务异常体系
|
||||||
|
|
||||||
|
项目使用一个共同基础异常:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LibraryError(Exception):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
具体异常继承它:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BookNotFoundError(LibraryError):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
调用者既可以捕获具体错误,也可以统一处理所有图书馆业务错误:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
library.borrow_book(...)
|
||||||
|
except LibraryError as error:
|
||||||
|
print(error)
|
||||||
|
```
|
||||||
|
|
||||||
|
与 Java 不同,Python 没有受检异常的强制 `throws` 声明。
|
||||||
|
|
||||||
|
## 六、借书事务中的一致性
|
||||||
|
|
||||||
|
一次成功借书必须同时更新:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Book.available = False
|
||||||
|
Member.borrowed_isbns 添加 ISBN
|
||||||
|
发送成功通知
|
||||||
|
```
|
||||||
|
|
||||||
|
更新前要先完成全部业务校验。如果先修改一部分状态再发现错误,容易留下不一致数据。
|
||||||
|
|
||||||
|
本课数据都在内存中,因此暂不涉及数据库事务。第四阶段学习数据库后,会把这里的业务模型升级为持久化版本。
|
||||||
|
|
||||||
|
## 七、鸭子类型通知器
|
||||||
|
|
||||||
|
`Library` 不要求通知器继承指定父类,只调用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
notifier.send(message)
|
||||||
|
```
|
||||||
|
|
||||||
|
未来可以传入邮件通知器、短信通知器或测试通知器,只要它们提供兼容的 `send()` 方法。
|
||||||
|
|
||||||
|
不要在业务类中写:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if isinstance(notifier, ConsoleNotifier):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
这会让业务类依赖具体实现,降低扩展能力。
|
||||||
|
|
||||||
|
## 八、参考示例
|
||||||
|
|
||||||
|
先运行不同业务场景的仓库示例:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\03_面向对象\3_5_面向对象综合项目\domain_model_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
示例展示:
|
||||||
|
|
||||||
|
- dataclass 数据对象;
|
||||||
|
- property 计算状态;
|
||||||
|
- 普通业务类通过组合管理对象;
|
||||||
|
- 自定义异常;
|
||||||
|
- 操作前校验和状态修改。
|
||||||
|
|
||||||
|
示例不是综合项目答案,只用于参考对象职责和代码组织方式。
|
||||||
|
|
||||||
|
## 九、完成综合项目
|
||||||
|
|
||||||
|
打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
03_面向对象/3_5_面向对象综合项目/practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
题目已经按实现顺序提供:
|
||||||
|
|
||||||
|
1. 业务异常;
|
||||||
|
2. `Book`;
|
||||||
|
3. `Member`;
|
||||||
|
4. 通知器;
|
||||||
|
5. `Library` 数据结构;
|
||||||
|
6. 添加与注册;
|
||||||
|
7. 内部查找;
|
||||||
|
8. 借书;
|
||||||
|
9. 还书;
|
||||||
|
10. 可借查询;
|
||||||
|
11. 正常流程;
|
||||||
|
12. 异常流程。
|
||||||
|
|
||||||
|
可以直接从 `practice.py` 开始,不必先完整阅读本文。
|
||||||
|
|
||||||
|
## 十、运行方法
|
||||||
|
|
||||||
|
完成代码后,在项目根目录运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\03_面向对象\3_5_面向对象综合项目\practice.py
|
||||||
|
```
|
||||||
|
|
||||||
|
每完成一两个类就运行一次,不要等全部写完再集中排错。
|
||||||
|
|
||||||
|
## 十一、常见错误
|
||||||
|
|
||||||
|
### 11.1 把所有行为放进一个类
|
||||||
|
|
||||||
|
如果 `Library` 同时负责格式化所有对象、直接保存通知历史和处理用户输入,职责会迅速膨胀。让每个对象管理与自己紧密相关的数据和行为。
|
||||||
|
|
||||||
|
### 11.2 使用继承表达拥有关系
|
||||||
|
|
||||||
|
图书馆拥有图书,不代表图书馆是一种图书。拥有关系使用组合。
|
||||||
|
|
||||||
|
### 11.3 校验前修改状态
|
||||||
|
|
||||||
|
先确认会员、图书和可借状态全部合法,再同时更新图书与会员。
|
||||||
|
|
||||||
|
### 11.4 捕获 Exception 隐藏程序错误
|
||||||
|
|
||||||
|
练习中优先捕获 `LibraryError`。直接捕获所有 `Exception` 可能把拼写错误等程序缺陷误当成业务失败。
|
||||||
|
|
||||||
|
### 11.5 为通知器判断具体类型
|
||||||
|
|
||||||
|
直接调用 `notifier.send()`,保持鸭子类型。不要为每种通知方式增加一个条件分支。
|
||||||
|
|
||||||
|
## 十二、验证层级
|
||||||
|
|
||||||
|
完成后按以下层级检查:
|
||||||
|
|
||||||
|
- 必须修复:语法和运行错误、状态不一致、业务规则遗漏、共享借阅列表、具体类型分支;
|
||||||
|
- 建议改进:重复查找、职责混乱、容易误解的结构;
|
||||||
|
- 可选优化:非必要类型注解、说明文字差异、输出美化和格式统一。
|
||||||
|
|
||||||
|
只有“必须修复”会阻止项目和第三阶段验收。
|
||||||
|
|
||||||
|
## 十三、第三阶段知识回顾
|
||||||
|
|
||||||
|
- `self` 显式出现在实例方法定义中;
|
||||||
|
- `__init__` 负责初始化,创建对象不使用 `new`;
|
||||||
|
- 下划线主要表达约定,不是 Java 访问控制的完全替代;
|
||||||
|
- property 保持属性语法并执行方法逻辑;
|
||||||
|
- `super()` 按 MRO 继续查找实现;
|
||||||
|
- 鸭子类型关注对象行为而非声明的共同接口;
|
||||||
|
- dataclass 减少数据对象样板代码;
|
||||||
|
- 特殊方法让对象参与 Python 标准语法;
|
||||||
|
- 组合通常比为了复用而继承更灵活。
|
||||||
|
|
||||||
|
## 十四、阶段验收标准
|
||||||
|
|
||||||
|
- `practice.py` 正常运行;
|
||||||
|
- 正常借还流程状态正确;
|
||||||
|
- 主要异常流程均被阻止;
|
||||||
|
- 类的职责边界清晰;
|
||||||
|
- 能解释项目中继承、组合和鸭子类型分别出现在哪里;
|
||||||
|
- 能说明 dataclass 和普通类的选择原因;
|
||||||
|
- 能说明 Python 面向对象写法与 Java 的主要差异。
|
||||||
|
|
||||||
|
通过后,第三阶段完成,下一阶段开始数据库编程,并逐步把本项目升级为持久化版本。
|
||||||
82
03_面向对象/3_5_面向对象综合项目/domain_model_example.py
Normal file
82
03_面向对象/3_5_面向对象综合项目/domain_model_example.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# 第 3-5 课参考示例:小型仓库领域模型
|
||||||
|
#
|
||||||
|
# 本示例使用与综合项目不同的“仓库”场景,演示如何让对象各自负责自己的数据和行为。
|
||||||
|
# 综合项目仍需独立完成,不能直接复制本示例得到答案。
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryError(Exception):
|
||||||
|
"""表示仓库业务规则错误。"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Product:
|
||||||
|
"""保存商品数据,并负责与商品自身有关的显示。"""
|
||||||
|
|
||||||
|
code: str
|
||||||
|
name: str
|
||||||
|
quantity: int = 0
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""确保创建商品时库存数量合法。"""
|
||||||
|
if self.quantity < 0:
|
||||||
|
raise InventoryError("库存数量不能小于 0。")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_stock(self) -> bool:
|
||||||
|
"""根据当前数量计算是否有库存。"""
|
||||||
|
return self.quantity > 0
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""返回面向使用者的商品信息。"""
|
||||||
|
return f"{self.name}|库存:{self.quantity}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Warehouse:
|
||||||
|
"""通过组合管理多个 Product 对象。"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
products: dict[str, Product] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def add_product(self, product: Product) -> None:
|
||||||
|
"""添加商品,商品编码重复时阻止操作。"""
|
||||||
|
if product.code in self.products:
|
||||||
|
raise InventoryError("商品编码已存在。")
|
||||||
|
self.products[product.code] = product
|
||||||
|
|
||||||
|
def remove_stock(self, code: str, quantity: int) -> None:
|
||||||
|
"""扣减指定商品库存。"""
|
||||||
|
if code not in self.products:
|
||||||
|
raise InventoryError("商品不存在。")
|
||||||
|
|
||||||
|
product = self.products[code]
|
||||||
|
if quantity <= 0:
|
||||||
|
raise InventoryError("出库数量必须大于 0。")
|
||||||
|
if product.quantity < quantity:
|
||||||
|
raise InventoryError("库存不足。")
|
||||||
|
|
||||||
|
product.quantity -= quantity
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""运行仓库领域模型示例。"""
|
||||||
|
warehouse = Warehouse("教学仓库")
|
||||||
|
keyboard = Product("P001", "机械键盘", 3)
|
||||||
|
|
||||||
|
warehouse.add_product(keyboard)
|
||||||
|
print(keyboard)
|
||||||
|
print(f"是否有库存:{keyboard.in_stock}")
|
||||||
|
|
||||||
|
warehouse.remove_stock("P001", 2)
|
||||||
|
print(f"出库后:{keyboard}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
warehouse.remove_stock("P001", 2)
|
||||||
|
except InventoryError as error:
|
||||||
|
print(f"业务操作失败:{error}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
177
03_面向对象/3_5_面向对象综合项目/practice.py
Normal file
177
03_面向对象/3_5_面向对象综合项目/practice.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# 第 3-5 课综合项目:内存版图书管理系统
|
||||||
|
#
|
||||||
|
# 本项目只使用内存数据,不读写文件、不连接数据库。
|
||||||
|
# 请按照题目顺序实现,不要删除业务规则、测试流程、预期输出和验收说明。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:定义业务异常
|
||||||
|
# 1. 定义 LibraryError(Exception),类体中只写 pass。
|
||||||
|
# 2. 定义 BookNotFoundError(LibraryError),类体中只写 pass。
|
||||||
|
# 3. 定义 BookUnavailableError(LibraryError),类体中只写 pass。
|
||||||
|
# 4. 定义 MemberNotFoundError(LibraryError),类体中只写 pass。
|
||||||
|
#
|
||||||
|
# Python 与 Java 对照:
|
||||||
|
# - 自定义异常同样通过继承建立分类;
|
||||||
|
# - Python 没有 Java 受检异常(Checked Exception)的强制 throws 声明;
|
||||||
|
# - 调用者可以捕获具体异常,也可以统一捕获 LibraryError。
|
||||||
|
|
||||||
|
|
||||||
|
# 第二部分:定义 Book 数据类
|
||||||
|
# 1. 使用 @dataclass。
|
||||||
|
# 2. 定义字段:
|
||||||
|
# isbn: str
|
||||||
|
# title: str
|
||||||
|
# author: str
|
||||||
|
# available: bool = True
|
||||||
|
# 3. 定义 __str__,返回格式:
|
||||||
|
# “《Python 编程》|作者:小明|状态:可借”。
|
||||||
|
# 4. available 为 False 时,状态文字应为“已借出”。
|
||||||
|
#
|
||||||
|
# 设计提醒:
|
||||||
|
# - dataclass 自动生成初始化、调试显示和字段相等比较;
|
||||||
|
# - __str__ 只负责一个 Book 自身的友好显示。
|
||||||
|
|
||||||
|
|
||||||
|
# 第三部分:定义 Member 数据类
|
||||||
|
# 1. 使用 @dataclass。
|
||||||
|
# 2. 定义字段:
|
||||||
|
# member_id: str
|
||||||
|
# name: str
|
||||||
|
# borrowed_isbns: list[str] = field(default_factory=list)
|
||||||
|
# 3. 定义 borrowed_count 只读 property,返回已借 ISBN 的数量。
|
||||||
|
#
|
||||||
|
# 设计提醒:
|
||||||
|
# - 使用 default_factory,避免不同会员共享同一借阅列表;
|
||||||
|
# - Member 只保存自己的借阅结果,不负责查找图书馆中的书。
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:定义 ConsoleNotifier
|
||||||
|
# 1. 不需要继承任何通知父类或接口。
|
||||||
|
# 2. 定义 send(self, message) 方法,直接输出:
|
||||||
|
# “通知:{message}”。
|
||||||
|
#
|
||||||
|
# 鸭子类型提醒:
|
||||||
|
# - Library 只要求通知器具有 send() 方法;
|
||||||
|
# - 不要在 Library 中使用 isinstance() 判断通知器类型。
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:定义 Library 类和初始化方法
|
||||||
|
# 1. 定义 __init__(self, name) 方法并保存 self.name。
|
||||||
|
# 2. 创建实例属性:
|
||||||
|
# self.books: dict[str, Book] = {}
|
||||||
|
# self.members: dict[str, Member] = {}
|
||||||
|
# 3. books 使用 ISBN 作为键,members 使用会员编号作为键。
|
||||||
|
#
|
||||||
|
# 组合提醒:
|
||||||
|
# - Library “拥有”多个 Book 和 Member,因此这里使用组合而不是继承;
|
||||||
|
# - 不要让 Library 继承 list 或 dict。
|
||||||
|
|
||||||
|
|
||||||
|
# 第六部分:实现 add_book(self, book)
|
||||||
|
# 1. book.isbn 已存在时抛出 LibraryError("ISBN 已存在。")。
|
||||||
|
# 2. 不存在时把 book 保存到 self.books。
|
||||||
|
# 3. 本方法不需要输出。
|
||||||
|
|
||||||
|
|
||||||
|
# 第七部分:实现 register_member(self, member)
|
||||||
|
# 1. member.member_id 已存在时抛出 LibraryError("会员编号已存在。")。
|
||||||
|
# 2. 不存在时把 member 保存到 self.members。
|
||||||
|
# 3. 本方法不需要输出。
|
||||||
|
|
||||||
|
|
||||||
|
# 第八部分:实现两个内部查找方法
|
||||||
|
# 1. 定义 _get_book(self, isbn),找到时返回 Book。
|
||||||
|
# 2. 找不到时抛出 BookNotFoundError("图书不存在。")。
|
||||||
|
# 3. 定义 _get_member(self, member_id),找到时返回 Member。
|
||||||
|
# 4. 找不到时抛出 MemberNotFoundError("会员不存在。")。
|
||||||
|
#
|
||||||
|
# 单下划线提醒:
|
||||||
|
# - _get_book 和 _get_member 表示 Library 内部使用的辅助方法;
|
||||||
|
# - 单下划线是约定,不是 Java private 式的强制限制。
|
||||||
|
|
||||||
|
|
||||||
|
# 第九部分:实现 borrow_book(self, member_id, isbn, notifier)
|
||||||
|
# 1. 通过 _get_member() 查找会员。
|
||||||
|
# 2. 通过 _get_book() 查找图书。
|
||||||
|
# 3. book.available 为 False 时抛出:
|
||||||
|
# BookUnavailableError("图书已被借出。")。
|
||||||
|
# 4. 校验通过后执行:
|
||||||
|
# book.available = False
|
||||||
|
# member.borrowed_isbns.append(isbn)
|
||||||
|
# 5. 调用 notifier.send(),消息格式:
|
||||||
|
# “小明成功借阅《Python 编程》”。
|
||||||
|
#
|
||||||
|
# 多态提醒:
|
||||||
|
# - notifier 的类型注解可以省略,不影响验收;
|
||||||
|
# - 只调用 send(),不要判断通知器的具体类型。
|
||||||
|
|
||||||
|
|
||||||
|
# 第十部分:实现 return_book(self, member_id, isbn, notifier)
|
||||||
|
# 1. 通过两个内部查找方法取得会员和图书。
|
||||||
|
# 2. isbn 不在 member.borrowed_isbns 中时抛出:
|
||||||
|
# LibraryError("该会员没有借阅这本书。")。
|
||||||
|
# 3. 校验通过后执行:
|
||||||
|
# member.borrowed_isbns.remove(isbn)
|
||||||
|
# book.available = True
|
||||||
|
# 4. 调用 notifier.send(),消息格式:
|
||||||
|
# “小明成功归还《Python 编程》”。
|
||||||
|
|
||||||
|
|
||||||
|
# 第十一部分:实现 available_books(self)
|
||||||
|
# 1. 返回所有 available 为 True 的 Book 对象组成的新列表。
|
||||||
|
# 2. 不要修改 self.books。
|
||||||
|
# 3. 可以使用列表推导式。
|
||||||
|
|
||||||
|
|
||||||
|
# 第十二部分:定义 main() 并完成正常流程
|
||||||
|
# 1. 创建 Library("城市图书馆")。
|
||||||
|
# 2. 创建 ConsoleNotifier()。
|
||||||
|
# 3. 添加以下两本书:
|
||||||
|
# Book("978-001", "Python 编程", "小明")
|
||||||
|
# Book("978-002", "流畅的 Python", "Luciano")
|
||||||
|
# 4. 注册 Member("M001", "小红")。
|
||||||
|
# 5. 调用 library.available_books() 取得当前可借图书列表。
|
||||||
|
# 6. 使用 len() 统计列表长度并输出“初始可借数量:2”,参考写法:
|
||||||
|
# print(f"初始可借数量:{len(library.available_books())}")
|
||||||
|
# 7. 借出 ISBN 为 978-001 的书。
|
||||||
|
# 8. 再次调用 available_books(),使用 len() 输出“借阅后可借数量:1”。
|
||||||
|
# 9. 读取前面创建的 Member 对象的 borrowed_count,输出“会员已借数量:1”。
|
||||||
|
# 10. 归还 ISBN 为 978-001 的书。
|
||||||
|
# 11. 再次调用 available_books(),使用 len() 输出“归还后可借数量:2”。
|
||||||
|
# 12. 再次读取该 Member 对象的 borrowed_count,输出“会员已借数量:0”。
|
||||||
|
|
||||||
|
|
||||||
|
# 第十三部分:在 main() 中完成异常流程
|
||||||
|
# 1. 再次借出 978-001。
|
||||||
|
# 2. 使用 try...except 再借一次同一本书。
|
||||||
|
# 3. 捕获 LibraryError,并输出:
|
||||||
|
# “重复借阅已被阻止:图书已被借出。”。
|
||||||
|
# 4. 使用 try...except 借阅 ISBN 为 978-999 的书。
|
||||||
|
# 5. 捕获 LibraryError,并输出:
|
||||||
|
# “不存在的图书已被阻止:图书不存在。”。
|
||||||
|
# 6. 添加程序入口判断并调用 main()。
|
||||||
|
#
|
||||||
|
# 完整预期输出:
|
||||||
|
# 初始可借数量:2
|
||||||
|
# 通知:小红成功借阅《Python 编程》
|
||||||
|
# 借阅后可借数量:1
|
||||||
|
# 会员已借数量:1
|
||||||
|
# 通知:小红成功归还《Python 编程》
|
||||||
|
# 归还后可借数量:2
|
||||||
|
# 会员已借数量:0
|
||||||
|
# 通知:小红成功借阅《Python 编程》
|
||||||
|
# 重复借阅已被阻止:图书已被借出。
|
||||||
|
# 不存在的图书已被阻止:图书不存在。
|
||||||
|
|
||||||
|
|
||||||
|
# 最终验收标准:
|
||||||
|
# 1. Book 和 Member 使用 dataclass,且不同会员不共享借阅列表;
|
||||||
|
# 2. Book 的 __str__ 能区分“可借”和“已借出”;
|
||||||
|
# 3. Member.borrowed_count 能随借还操作变化;
|
||||||
|
# 4. Library 通过组合管理 Book 和 Member;
|
||||||
|
# 5. 借书和还书同时更新图书状态与会员借阅记录;
|
||||||
|
# 6. 通知器通过鸭子类型调用,没有具体类型判断;
|
||||||
|
# 7. 业务异常继承关系正确,并能统一捕获 LibraryError;
|
||||||
|
# 8. 重复 ISBN、重复会员、图书不存在、会员不存在、重复借阅和错误归还均被阻止;
|
||||||
|
# 9. available_books() 返回新列表,不修改图书字典;
|
||||||
|
# 10. 程序实际输出的业务结果正确;说明文字或非必要类型注解不阻塞验收。
|
||||||
286
04_数据库/4_1_PostgreSQL与Psycopg入门/README.md
Normal file
286
04_数据库/4_1_PostgreSQL与Psycopg入门/README.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# 第 4-1 课:PostgreSQL 与 Psycopg 入门
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
你已经掌握 SQL、事务和 Java 数据库开发,因此本课不再从表、字段和增删改查讲起,而是集中回答一个问题:Python 程序怎样安全地连接 PostgreSQL 并执行 SQL?
|
||||||
|
|
||||||
|
Python 数据库 API(Database API,DB-API)规定了数据库驱动的通用操作方式。Psycopg 3 是 PostgreSQL 的 Python 驱动,本课会把它与 JDBC 逐项对照。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 解释 DB-API、Psycopg 和 PostgreSQL 的关系;
|
||||||
|
2. 使用本地 TOML 文件保存数据库连接配置;
|
||||||
|
3. 使用 `psycopg.connect()` 创建连接;
|
||||||
|
4. 使用游标执行只读 SQL 并取得结果;
|
||||||
|
5. 使用参数化查询传递数据;
|
||||||
|
6. 使用 `with` 自动释放连接和游标;
|
||||||
|
7. 对照 JDBC 理解 Python 数据库代码;
|
||||||
|
8. 识别连接失败、依赖缺失和参数占位符错误。
|
||||||
|
|
||||||
|
## 三、前置知识
|
||||||
|
|
||||||
|
本课默认已经掌握:
|
||||||
|
|
||||||
|
- PostgreSQL 数据库地址、端口、数据库、用户名和密码的含义;
|
||||||
|
- `SELECT` 基础语法;
|
||||||
|
- JDBC 的 `Connection`、`PreparedStatement` 和 `ResultSet`;
|
||||||
|
- Python 函数、异常、上下文管理器和文件读取基础。
|
||||||
|
|
||||||
|
本课只连接专用练习数据库。不要连接生产数据库,也不要使用具有创建用户、删除数据库等高权限的账号。
|
||||||
|
|
||||||
|
## 四、DB-API、Psycopg 与 JDBC
|
||||||
|
|
||||||
|
DB-API 是 Python 数据库驱动遵循的接口约定,不是一个需要单独安装的框架。不同数据库有不同驱动,但常见操作方式比较统一。
|
||||||
|
|
||||||
|
| Java/JDBC | Python/Psycopg | 作用 |
|
||||||
|
|---|---|---|
|
||||||
|
| PostgreSQL JDBC Driver | Psycopg 3 | 与 PostgreSQL 通信 |
|
||||||
|
| `DriverManager.getConnection()` | `psycopg.connect()` | 创建数据库连接 |
|
||||||
|
| `Connection` | `Connection` | 表示一次数据库会话 |
|
||||||
|
| `PreparedStatement` | `Cursor.execute(sql, params)` | 执行参数化 SQL |
|
||||||
|
| `ResultSet` | `Cursor` 和 `fetchone()` 等方法 | 读取查询结果 |
|
||||||
|
| `try-with-resources` | `with` | 自动释放资源 |
|
||||||
|
| `SQLException` | `psycopg.Error` | 表示数据库访问错误 |
|
||||||
|
|
||||||
|
Psycopg 的游标(Cursor)同时承担“执行 SQL”和“读取结果”的职责。它不是数据库界面中的鼠标光标。
|
||||||
|
|
||||||
|
## 五、准备远程练习数据库
|
||||||
|
|
||||||
|
建议为课程准备:
|
||||||
|
|
||||||
|
- 一个独立数据库,例如 `python_course`;
|
||||||
|
- 一个专用账号,例如 `python_student`;
|
||||||
|
- 只授予课程所需权限;
|
||||||
|
- 不与生产环境或其他重要测试数据共用。
|
||||||
|
|
||||||
|
本课不使用环境变量,也不要求把所有信息拼成数据库连接串,而是把连接参数分字段写入 TOML 配置:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[postgresql]
|
||||||
|
host = "数据库主机"
|
||||||
|
port = 5432
|
||||||
|
dbname = "数据库名"
|
||||||
|
user = "用户名"
|
||||||
|
password = "密码"
|
||||||
|
connect_timeout = 10
|
||||||
|
```
|
||||||
|
|
||||||
|
仓库中的 [config.example.toml](./config.example.toml) 只包含占位内容,可以提交 Git。实际配置写入同目录的 `config.toml`,该文件已经被项目 `.gitignore` 排除。
|
||||||
|
|
||||||
|
TOML(Tom's Obvious Minimal Language)是一种结构化配置格式。Python 3.11 及以上版本内置 `tomllib`,读取 TOML 不需要安装额外依赖,也不会修改操作系统或当前进程的环境变量。
|
||||||
|
|
||||||
|
## 六、安装 Psycopg 3
|
||||||
|
|
||||||
|
你当前使用 Conda 的 `base` 环境,可以直接安装 Psycopg:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda install -n base -c conda-forge "psycopg>=3,<4" psycopg-c
|
||||||
|
```
|
||||||
|
|
||||||
|
安装完成后验证:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -c "import psycopg; print(psycopg.__version__)"
|
||||||
|
```
|
||||||
|
|
||||||
|
如果以后改用 Python 虚拟环境,也可以使用 `requirements.txt` 安装。无论使用哪种方式,导入时都写 `import psycopg`,不是 `import psycopg3`。
|
||||||
|
|
||||||
|
## 七、创建本地 TOML 配置
|
||||||
|
|
||||||
|
进入本课目录,复制配置模板:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .\config.example.toml .\config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
然后只在本地 `config.toml` 中填写真实的主机、端口、数据库名、用户名和密码。程序通过 Python 文件自身的位置寻找配置,因此从项目根目录或课程目录启动都可以。
|
||||||
|
|
||||||
|
### 7.1 为什么选择 TOML
|
||||||
|
|
||||||
|
- Python 3.13 可以直接使用内置 `tomllib`;
|
||||||
|
- 字段和类型清楚,端口可以保持整数;
|
||||||
|
- 不需要污染环境变量;
|
||||||
|
- 比 XML 简洁,比 YAML 少一个第三方解析依赖;
|
||||||
|
- 配置节结构与 Java 项目的 YAML、Properties 配置思路相近。
|
||||||
|
|
||||||
|
### 7.2 代码硬编码可以怎么写
|
||||||
|
|
||||||
|
从技术上可以直接构造字典:
|
||||||
|
|
||||||
|
```python
|
||||||
|
database_config = {
|
||||||
|
"host": "数据库主机",
|
||||||
|
"port": 5432,
|
||||||
|
"dbname": "数据库名",
|
||||||
|
"user": "用户名",
|
||||||
|
"password": "密码",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这能帮助理解 `psycopg.connect()` 接收哪些参数,但真实密码一旦硬编码(Hard Coding)就可能进入 Git 历史。本课程标准示例使用 `config.toml`;如自行尝试硬编码,只能放在不提交的个人练习文件中。
|
||||||
|
|
||||||
|
## 八、完整示例
|
||||||
|
|
||||||
|
示例文件为 [connection_example.py](./connection_example.py)。核心结构如下:
|
||||||
|
|
||||||
|
```python
|
||||||
|
database_config = load_database_config(CONFIG_PATH)
|
||||||
|
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT current_database(), current_user, %s::text",
|
||||||
|
("Psycopg 连接成功",),
|
||||||
|
)
|
||||||
|
database_name, user_name, message = cursor.fetchone()
|
||||||
|
```
|
||||||
|
|
||||||
|
示例只读取当前数据库名和当前用户,不创建表、不修改数据。
|
||||||
|
|
||||||
|
### 8.1 为什么参数使用 `%s`
|
||||||
|
|
||||||
|
Psycopg 使用 `%s` 表示值参数,即使参数是整数也仍然使用 `%s`。参数值通过 `execute()` 的第二个参数单独传入:
|
||||||
|
|
||||||
|
```python
|
||||||
|
cursor.execute("SELECT %s::text", (message,))
|
||||||
|
```
|
||||||
|
|
||||||
|
不要使用 f-string、字符串拼接或 `%` 运算符把数据直接写进 SQL:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误示例:数据被直接拼进 SQL,可能产生 SQL 注入。
|
||||||
|
cursor.execute(f"SELECT '{message}'")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 单个参数为什么有逗号
|
||||||
|
|
||||||
|
```python
|
||||||
|
(message,)
|
||||||
|
```
|
||||||
|
|
||||||
|
这是只有一个元素的元组。写成 `(message)` 只是在字符串外加括号,不是元组。
|
||||||
|
|
||||||
|
### 8.3 with 做了什么
|
||||||
|
|
||||||
|
- 离开游标的 `with` 时关闭游标;
|
||||||
|
- 离开连接的 `with` 时结束事务并关闭连接;
|
||||||
|
- 正常离开连接块时提交当前事务;
|
||||||
|
- 块内出现异常时回滚当前事务。
|
||||||
|
|
||||||
|
本课执行的是只读查询,但仍要建立正确的资源和事务管理习惯。
|
||||||
|
|
||||||
|
## 九、运行方法
|
||||||
|
|
||||||
|
确认 `config.toml` 已经创建并填写完成,然后在项目根目录运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python .\04_数据库\4_1_PostgreSQL与Psycopg入门\connection_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
正常情况下会看到类似结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
连接成功。
|
||||||
|
当前数据库:python_course
|
||||||
|
当前用户:python_student
|
||||||
|
参数化查询结果:Psycopg 连接成功
|
||||||
|
```
|
||||||
|
|
||||||
|
数据库名和用户名应以你的远程练习环境为准。
|
||||||
|
|
||||||
|
## 十、关键执行顺序
|
||||||
|
|
||||||
|
1. `main()` 调用 `load_database_config(CONFIG_PATH)`;
|
||||||
|
2. `Path` 根据当前 Python 文件的位置定位 `config.toml`;
|
||||||
|
3. `tomllib.load()` 读取 `[postgresql]` 配置节;
|
||||||
|
4. 缺少文件、配置节或必填字段时主动抛出中文 `RuntimeError`;
|
||||||
|
5. `psycopg.connect(**database_config)` 把字典展开为连接参数;
|
||||||
|
6. `connection.cursor()` 创建游标;
|
||||||
|
7. `cursor.execute()` 将 SQL 和查询参数分别交给驱动;
|
||||||
|
8. `cursor.fetchone()` 读取一行结果;
|
||||||
|
9. 两层 `with` 依次释放游标和连接;
|
||||||
|
10. `main()` 输出结果,并分别处理配置异常和数据库异常。
|
||||||
|
|
||||||
|
## 十一、常见错误
|
||||||
|
|
||||||
|
### 11.1 `ModuleNotFoundError: No module named 'psycopg'`
|
||||||
|
|
||||||
|
含义:当前 Python 环境没有安装 Psycopg。
|
||||||
|
|
||||||
|
处理:激活 `.venv`,再使用本课的 `requirements.txt` 安装依赖。可以运行 `python -m pip show psycopg` 检查安装位置。
|
||||||
|
|
||||||
|
### 11.2 未找到 `config.toml`
|
||||||
|
|
||||||
|
含义:本课目录中还没有实际配置文件。
|
||||||
|
|
||||||
|
处理:把 `config.example.toml` 复制为 `config.toml` 并填写连接参数。不要直接把真实信息写进示例模板。
|
||||||
|
|
||||||
|
### 11.3 TOML 格式错误
|
||||||
|
|
||||||
|
含义:配置不符合 TOML 语法,例如字符串缺少引号或同一个键重复出现。
|
||||||
|
|
||||||
|
处理:对照 `config.example.toml` 检查配置节、等号、引号和字段名。
|
||||||
|
|
||||||
|
### 11.4 `connection refused` 或连接超时
|
||||||
|
|
||||||
|
含义:程序无法到达数据库地址和端口。
|
||||||
|
|
||||||
|
检查数据库服务、主机名、端口、防火墙、白名单和 VPN,不要先假设一定是密码错误。
|
||||||
|
|
||||||
|
### 11.5 `password authentication failed`
|
||||||
|
|
||||||
|
含义:服务器已经收到连接,但用户名或密码校验失败。
|
||||||
|
|
||||||
|
检查 `config.toml` 中的账号、密码和数据库名。分字段传参时无需手动拼接 URL,也避免了连接串中特殊字符编码问题。
|
||||||
|
|
||||||
|
### 11.6 `execute()` 参数写错
|
||||||
|
|
||||||
|
下面两种写法都不符合本课要求:
|
||||||
|
|
||||||
|
```python
|
||||||
|
cursor.execute("SELECT '%s'", (message,))
|
||||||
|
cursor.execute("SELECT %s", message)
|
||||||
|
```
|
||||||
|
|
||||||
|
占位符外不要加引号,参数序列只有一个值时要写成 `(message,)`。
|
||||||
|
|
||||||
|
### 11.7 使用 `fetchone()` 却没有处理空结果
|
||||||
|
|
||||||
|
`fetchone()` 在没有数据时可能返回 `None`。本课查询必然返回一行,所以可以直接解包;以后查询业务表时必须判断空结果。
|
||||||
|
|
||||||
|
## 十二、课堂练习
|
||||||
|
|
||||||
|
打开 [practice.py](./practice.py),按照注释完成练习。练习要求你独立完成:
|
||||||
|
|
||||||
|
1. 使用 `Path` 定位本地 `config.toml`;
|
||||||
|
2. 使用 `tomllib` 读取 PostgreSQL 配置;
|
||||||
|
3. 使用两层 `with` 管理连接和游标;
|
||||||
|
4. 执行包含两个参数的只读查询;
|
||||||
|
5. 使用 `fetchone()` 保存并输出结果;
|
||||||
|
6. 分别捕获配置读取异常和数据库访问异常。
|
||||||
|
|
||||||
|
练习仍然只执行只读 SQL,不创建、修改或删除远程数据。
|
||||||
|
|
||||||
|
## 十三、本课小结
|
||||||
|
|
||||||
|
- DB-API 是 Python 数据库驱动的通用接口约定;
|
||||||
|
- Psycopg 3 是 PostgreSQL 的 Python 驱动;
|
||||||
|
- Psycopg 的基础层次与 JDBC 相似;
|
||||||
|
- `Connection` 表示数据库会话,`Cursor` 负责执行 SQL 和读取结果;
|
||||||
|
- SQL 与参数必须分开传递;
|
||||||
|
- `with` 用于可靠地结束事务和释放资源;
|
||||||
|
- 数据库密码保存在被 Git 忽略的本地 TOML 配置中,不写入环境变量或源码。
|
||||||
|
|
||||||
|
## 十四、验收标准
|
||||||
|
|
||||||
|
- 能说明 DB-API、Psycopg 和 PostgreSQL 的关系;
|
||||||
|
- 能说出 Psycopg 与 JDBC 的主要对象对应关系;
|
||||||
|
- 能复制模板并通过本地 `config.toml` 配置连接参数;
|
||||||
|
- `connection_example.py` 可以连接练习数据库并输出三项查询结果;
|
||||||
|
- `practice.py` 使用参数化查询,没有拼接 SQL;
|
||||||
|
- 连接和游标都通过 `with` 管理;
|
||||||
|
- 能区分网络不可达、认证失败和依赖缺失;
|
||||||
|
- 程序没有读取、设置或修改环境变量;
|
||||||
|
- 未把 `config.toml` 或真实数据库连接信息写入 Git 跟踪文件。
|
||||||
10
04_数据库/4_1_PostgreSQL与Psycopg入门/config.example.toml
Normal file
10
04_数据库/4_1_PostgreSQL与Psycopg入门/config.example.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 复制本文件并重命名为 config.toml,再填写本地练习数据库信息。
|
||||||
|
# config.toml 已加入 .gitignore,不会被 Git 跟踪。
|
||||||
|
|
||||||
|
[postgresql]
|
||||||
|
host = "数据库主机"
|
||||||
|
port = 5432
|
||||||
|
dbname = "数据库名"
|
||||||
|
user = "用户名"
|
||||||
|
password = "密码"
|
||||||
|
connect_timeout = 10
|
||||||
86
04_数据库/4_1_PostgreSQL与Psycopg入门/connection_example.py
Normal file
86
04_数据库/4_1_PostgreSQL与Psycopg入门/connection_example.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"""第 4-1 课示例:从 TOML 配置读取参数并连接 PostgreSQL。"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
|
||||||
|
|
||||||
|
# 使用当前 Python 文件的位置定位配置,避免程序依赖 PowerShell 的工作目录。
|
||||||
|
CONFIG_PATH = Path(__file__).with_name("config.toml")
|
||||||
|
|
||||||
|
|
||||||
|
def load_database_config(config_path: Path) -> dict[str, str | int]:
|
||||||
|
"""读取并检查 TOML 中的 PostgreSQL 连接配置。"""
|
||||||
|
if not config_path.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
"未找到 config.toml,请复制 config.example.toml 并填写练习数据库配置。"
|
||||||
|
)
|
||||||
|
|
||||||
|
# tomllib 要求以二进制模式读取 TOML 文件,因此这里使用 "rb"。
|
||||||
|
with config_path.open("rb") as config_file:
|
||||||
|
config_data = tomllib.load(config_file)
|
||||||
|
|
||||||
|
postgresql_config = config_data.get("postgresql")
|
||||||
|
if not isinstance(postgresql_config, dict):
|
||||||
|
raise RuntimeError("config.toml 缺少 [postgresql] 配置节。")
|
||||||
|
|
||||||
|
required_names = ("host", "port", "dbname", "user", "password")
|
||||||
|
missing_names = [
|
||||||
|
name for name in required_names if postgresql_config.get(name) in (None, "")
|
||||||
|
]
|
||||||
|
if missing_names:
|
||||||
|
missing_text = "、".join(missing_names)
|
||||||
|
raise RuntimeError(f"config.toml 缺少数据库配置:{missing_text}。")
|
||||||
|
|
||||||
|
return postgresql_config
|
||||||
|
|
||||||
|
|
||||||
|
def query_connection_info(
|
||||||
|
database_config: dict[str, str | int],
|
||||||
|
) -> tuple[str, str, str]:
|
||||||
|
"""连接 PostgreSQL,执行只读参数化查询,并返回连接信息。"""
|
||||||
|
message = "Psycopg 连接成功"
|
||||||
|
|
||||||
|
# ** 会把字典中的键值展开为关键字参数,例如 host="数据库主机"。
|
||||||
|
# 连接信息来自本地 config.toml,不会写入环境变量。
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
# 第二层 with 管理游标。游标同时负责执行 SQL 和读取查询结果。
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# SQL 和参数必须分开传递,不能使用 f-string 拼接用户数据。
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT current_database(), current_user, %s::text",
|
||||||
|
(message,),
|
||||||
|
)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
# 这条 PostgreSQL 查询必然返回一行。普通业务查询仍要考虑 None。
|
||||||
|
if result is None:
|
||||||
|
raise RuntimeError("数据库没有返回连接验证结果。")
|
||||||
|
|
||||||
|
database_name, user_name, returned_message = result
|
||||||
|
return database_name, user_name, returned_message
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""组织配置读取、数据库查询和结果输出。"""
|
||||||
|
try:
|
||||||
|
database_config = load_database_config(CONFIG_PATH)
|
||||||
|
database_name, user_name, message = query_connection_info(database_config)
|
||||||
|
except (OSError, tomllib.TOMLDecodeError, RuntimeError) as error:
|
||||||
|
# 这里处理文件读取、TOML 格式以及课程程序主动检查到的问题。
|
||||||
|
print(f"配置读取失败:{error}")
|
||||||
|
return
|
||||||
|
except psycopg.Error as error:
|
||||||
|
# Psycopg 的具体异常信息可以保留英文,前面补充中文场景说明。
|
||||||
|
print(f"数据库访问失败:{error}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("连接成功。")
|
||||||
|
print(f"当前数据库:{database_name}")
|
||||||
|
print(f"当前用户:{user_name}")
|
||||||
|
print(f"参数化查询结果:{message}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
93
04_数据库/4_1_PostgreSQL与Psycopg入门/practice.py
Normal file
93
04_数据库/4_1_PostgreSQL与Psycopg入门/practice.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 第 4-1 课练习:从 TOML 配置连接 PostgreSQL
|
||||||
|
#
|
||||||
|
# 本文件只提供题目,不包含导入、代码骨架、测试数据或参考答案。
|
||||||
|
# 请完成只读查询,不要创建表,也不要新增、修改或删除远程数据库中的数据。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:准备并定位配置文件
|
||||||
|
# 1. 导入 pathlib.Path、tomllib 和 psycopg。
|
||||||
|
# 2. 使用 Path(__file__).with_name("config.toml") 得到配置文件路径,
|
||||||
|
# 并保存为 CONFIG_PATH。
|
||||||
|
# 3. 复制 config.example.toml,重命名为 config.toml,再填写本地连接信息。
|
||||||
|
# 4. 不得修改环境变量,也不得把真实连接信息写入 Python 文件。
|
||||||
|
#
|
||||||
|
# 与 Java 对照:
|
||||||
|
# - config.toml 类似独立的 application.yml 或 properties 本地配置;
|
||||||
|
# - CONFIG_PATH 类似确定配置资源位置;
|
||||||
|
# - 本课手动读取配置,后续框架可能负责自动绑定配置对象。
|
||||||
|
|
||||||
|
# 第二部分:读取 TOML 配置
|
||||||
|
# 1. 定义 load_database_config(config_path) 函数。
|
||||||
|
# 2. config_path 不存在时抛出 RuntimeError,消息为:
|
||||||
|
# “未找到 config.toml,请先复制并填写配置文件。”。
|
||||||
|
# 3. 使用 config_path.open("rb") 和 with 打开文件。
|
||||||
|
# 4. 调用 tomllib.load(config_file),保存完整配置。
|
||||||
|
# 5. 使用配置节名称 postgresql 取得数据库配置字典。
|
||||||
|
# 6. 配置节不存在或不是字典时抛出:
|
||||||
|
# RuntimeError("config.toml 缺少 [postgresql] 配置节。")
|
||||||
|
# 7. 返回 postgresql 配置字典。
|
||||||
|
|
||||||
|
|
||||||
|
# 第三部分:执行参数化只读查询
|
||||||
|
# 1. 定义 query_lesson_info(database_config, lesson_name, lesson_number) 函数。
|
||||||
|
# 2. 调用 psycopg.connect(**database_config),并使用 with 管理 Connection。
|
||||||
|
# 3. 调用 connection.cursor(),并使用第二层 with 管理 Cursor。
|
||||||
|
# 4. 调用 cursor.execute() 执行:
|
||||||
|
# SELECT current_database(), %s::text, %s::integer
|
||||||
|
# 5. 把 lesson_name 和 lesson_number 组成二元素元组,作为 execute() 的第二个参数。
|
||||||
|
# 6. 不得使用 f-string、字符串拼接或 % 运算把参数拼进 SQL。
|
||||||
|
# 7. 调用 cursor.fetchone(),把返回值保存到 result。
|
||||||
|
# 8. 离开两层 with 后,如果 result 是 None,抛出:
|
||||||
|
# RuntimeError("数据库没有返回练习结果。")
|
||||||
|
# 9. 解包并返回 database_name、returned_name、returned_number。
|
||||||
|
|
||||||
|
|
||||||
|
# 第四部分:组织 main() 正常流程
|
||||||
|
# 1. 定义 main()。
|
||||||
|
# 2. 调用 load_database_config(CONFIG_PATH),保存 database_config。
|
||||||
|
# 3. 调用:
|
||||||
|
# query_lesson_info(database_config, "PostgreSQL 与 Psycopg 入门", 1)
|
||||||
|
# 4. 保存返回的三个结果。
|
||||||
|
# 5. 分别使用 print() 输出:
|
||||||
|
# “当前数据库:实际数据库名”
|
||||||
|
# “课程名称:PostgreSQL 与 Psycopg 入门”
|
||||||
|
# “课程编号:1”
|
||||||
|
|
||||||
|
|
||||||
|
# 第五部分:处理异常
|
||||||
|
# 1. 在 main() 中使用 try...except 包围配置读取和数据库查询。
|
||||||
|
# 2. 捕获 OSError、tomllib.TOMLDecodeError 和 RuntimeError,输出:
|
||||||
|
# “配置读取失败:{错误信息}”,然后结束 main()。
|
||||||
|
# 3. 捕获 psycopg.Error,输出“数据库访问失败:{错误信息}”,然后结束 main()。
|
||||||
|
# 4. 添加程序入口判断并调用 main()。
|
||||||
|
#
|
||||||
|
# 缺少配置文件时的预期输出:
|
||||||
|
# 配置读取失败:未找到 config.toml,请先复制并填写配置文件。
|
||||||
|
#
|
||||||
|
# 正确连接后的预期输出格式:
|
||||||
|
# 当前数据库:python_course
|
||||||
|
# 课程名称:PostgreSQL 与 Psycopg 入门
|
||||||
|
# 课程编号:1
|
||||||
|
#
|
||||||
|
# 注意:第一行的 python_course 只是示例,应以实际数据库名为准。
|
||||||
|
|
||||||
|
# 自查清单:
|
||||||
|
# 1. 是否通过当前 Python 文件位置定位 config.toml?
|
||||||
|
# 2. 是否没有读取、设置或修改任何环境变量?
|
||||||
|
# 3. 是否没有在 Python 文件中填写真实账号、密码和主机地址?
|
||||||
|
# 4. Connection 和 Cursor 是否都使用 with 管理?
|
||||||
|
# 5. 是否使用 **database_config 把配置字典传给 psycopg.connect()?
|
||||||
|
# 6. SQL 和参数是否通过 execute() 的两个参数分别传入?
|
||||||
|
# 7. 是否保存并检查了 fetchone() 的结果?
|
||||||
|
# 8. 是否分别处理配置异常和 psycopg.Error?
|
||||||
|
|
||||||
|
|
||||||
|
# 最终验收标准:
|
||||||
|
# 1. practice.py 可以通过 Python 语法检查;
|
||||||
|
# 2. 缺少 config.toml 时能输出指定的中文提示;
|
||||||
|
# 3. 配置正确时能连接 PostgreSQL 并输出数据库名、课程名和课程编号;
|
||||||
|
# 4. 查询全程只读,不产生远程数据库写入;
|
||||||
|
# 5. SQL 使用参数化查询,不存在字符串拼接;
|
||||||
|
# 6. 连接和游标能够可靠关闭;
|
||||||
|
# 7. 程序不读写环境变量;
|
||||||
|
# 8. config.toml 和真实连接信息没有进入 Git 跟踪文件。
|
||||||
3
04_数据库/4_1_PostgreSQL与Psycopg入门/requirements.txt
Normal file
3
04_数据库/4_1_PostgreSQL与Psycopg入门/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Psycopg 3 是 PostgreSQL 的 Python 数据库驱动。
|
||||||
|
# binary 额外依赖适合本地学习,可避免首次安装时配置 C 编译环境。
|
||||||
|
psycopg[binary]>=3,<4
|
||||||
420
04_数据库/4_2_Python数据库事务与数据访问层/README.md
Normal file
420
04_数据库/4_2_Python数据库事务与数据访问层/README.md
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# 第 4-2 课:Python 数据库事务与数据访问层
|
||||||
|
|
||||||
|
## 一、本课定位
|
||||||
|
|
||||||
|
第一课已经完成 PostgreSQL 连接、游标、参数化查询和资源释放。本课先系统复习事务的核心概念,再学习 Python/Psycopg 中的事务边界,以及如何把 SQL 从业务逻辑中分离。数据库基础不会展开成完整 SQL 课程,但事务是后续 ORM 和 Web 业务正确性的基础,必须讲清楚。
|
||||||
|
|
||||||
|
本课会写入远程练习数据库:示例创建专用表 `course_bank_account`,只重置并操作 `COURSE-` 前缀数据;练习使用另一张专用表 `course_wallet_account`,只操作 `PRACTICE-` 前缀数据。不要连接生产数据库。
|
||||||
|
|
||||||
|
## 二、本课目标
|
||||||
|
|
||||||
|
完成本课后,你将能够:
|
||||||
|
|
||||||
|
1. 说明事务是什么以及为什么需要事务;
|
||||||
|
2. 结合转账解释原子性、一致性、隔离性和持久性;
|
||||||
|
3. 区分提交、回滚、自动提交和事务失败状态;
|
||||||
|
4. 解释常见并发异常和事务隔离级别;
|
||||||
|
5. 解释 Psycopg 默认事务行为;
|
||||||
|
6. 使用连接上下文自动提交和回滚;
|
||||||
|
7. 理解为什么异常必须传播出事务上下文;
|
||||||
|
8. 使用 `executemany()` 批量执行同一条参数化 SQL;
|
||||||
|
9. 使用 `FOR UPDATE` 锁定待修改数据;
|
||||||
|
10. 使用 Repository 隔离 SQL;
|
||||||
|
11. 把业务规则放在 Service 中;
|
||||||
|
12. 让一次业务操作共享同一连接和事务;
|
||||||
|
13. 对照 JDBC、MyBatis 和 Spring 事务理解 Python实现。
|
||||||
|
|
||||||
|
## 三、事务是什么
|
||||||
|
|
||||||
|
事务(Transaction)是数据库中的一个工作单元:它包含一条或多条操作,这些操作应该作为一个不可分割的整体完成。
|
||||||
|
|
||||||
|
转账至少包含两条更新:
|
||||||
|
|
||||||
|
```text
|
||||||
|
账户A扣款200元
|
||||||
|
账户B入账200元
|
||||||
|
```
|
||||||
|
|
||||||
|
如果第一条成功、第二条失败,却保留了第一条结果,钱就凭空减少了。事务要求最终只能出现两种结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
全部成功 → 提交(COMMIT)
|
||||||
|
任一步失败 → 回滚(ROLLBACK),恢复到事务开始前
|
||||||
|
```
|
||||||
|
|
||||||
|
对应的SQL概念是:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE account SET balance = balance - 200 WHERE account_no = 'A';
|
||||||
|
UPDATE account SET balance = balance + 200 WHERE account_no = 'B';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
```
|
||||||
|
|
||||||
|
中间发生错误时执行:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ROLLBACK;
|
||||||
|
```
|
||||||
|
|
||||||
|
使用Psycopg时通常不需要手写`BEGIN`。驱动会按连接状态自动开始事务,代码负责正确划定提交和回滚边界。
|
||||||
|
|
||||||
|
## 四、事务的ACID特性
|
||||||
|
|
||||||
|
ACID是事务需要满足的四类核心性质。
|
||||||
|
|
||||||
|
### 4.1 原子性(Atomicity)
|
||||||
|
|
||||||
|
事务内的操作要么全部成功,要么全部失败。转账中的扣款和入账不能只保留其中一步。
|
||||||
|
|
||||||
|
本课的失败示例会先扣除50元,再主动抛出异常。最终余额不变,就是在验证原子性。
|
||||||
|
|
||||||
|
### 4.2 一致性(Consistency)
|
||||||
|
|
||||||
|
事务执行前后,数据都必须满足数据库约束和业务规则。例如:
|
||||||
|
|
||||||
|
- 余额不能小于0;
|
||||||
|
- 转账前后两个账户的余额总额不应无故变化;
|
||||||
|
- 目标账户必须存在;
|
||||||
|
- 主键、非空和检查约束仍然成立。
|
||||||
|
|
||||||
|
一致性不是只靠数据库自动保证。数据库约束、事务、锁和Service中的业务校验要共同工作。
|
||||||
|
|
||||||
|
### 4.3 隔离性(Isolation)
|
||||||
|
|
||||||
|
多个事务并发执行时,一个事务不应随意看到另一个尚未完成事务的中间状态。
|
||||||
|
|
||||||
|
假设账户A余额为1000元,两个请求同时转出800元。如果二者都先读取到1000,再分别扣款,就可能发生超额转账。事务隔离级别和行锁用于控制这种并发影响。
|
||||||
|
|
||||||
|
隔离不等于所有事务完全串行。隔离越强,并发冲突通常越少,但等待和资源成本可能越高。
|
||||||
|
|
||||||
|
### 4.4 持久性(Durability)
|
||||||
|
|
||||||
|
事务成功提交后,结果应该持久保存。程序退出或连接关闭后,再使用新连接查询,仍然能够看到已提交的余额。
|
||||||
|
|
||||||
|
本课使用独立连接回查成功转账结果,就是在直观验证持久性。
|
||||||
|
|
||||||
|
## 五、提交、回滚与事务状态
|
||||||
|
|
||||||
|
### 5.1 提交
|
||||||
|
|
||||||
|
`COMMIT`确认事务中的修改。提交成功后,其他事务才能按照隔离规则观察到这些结果,当前事务也不能再整体撤销。
|
||||||
|
|
||||||
|
### 5.2 回滚
|
||||||
|
|
||||||
|
`ROLLBACK`取消当前事务中尚未提交的修改。它不是反向执行一条新的补偿SQL,而是让数据库放弃本次事务的未提交结果。
|
||||||
|
|
||||||
|
### 5.3 PostgreSQL事务失败状态
|
||||||
|
|
||||||
|
PostgreSQL事务中的某条SQL失败后,当前事务通常进入失败状态。即使后面的SQL本身正确,也会收到类似错误:
|
||||||
|
|
||||||
|
```text
|
||||||
|
current transaction is aborted, commands ignored until end of transaction block
|
||||||
|
```
|
||||||
|
|
||||||
|
中文含义是:当前事务已经失败,在事务结束前忽略后续命令。此时必须回滚,或让Psycopg的连接上下文因异常退出并自动回滚。
|
||||||
|
|
||||||
|
### 5.4 自动提交
|
||||||
|
|
||||||
|
自动提交(Autocommit)表示每条独立SQL完成后立即提交。它适合部分不需要多语句原子性的操作,但无法把扣款和入账自动组合成一个事务。
|
||||||
|
|
||||||
|
Psycopg默认`autocommit=False`。本课保持默认行为,不开启自动提交。
|
||||||
|
|
||||||
|
## 六、隔离级别与并发异常
|
||||||
|
|
||||||
|
常见并发异常如下:
|
||||||
|
|
||||||
|
| 并发现象 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 脏读(Dirty Read) | 读取到其他事务尚未提交的数据;对方回滚后,读到的数据从未真正成立 |
|
||||||
|
| 不可重复读(Non-repeatable Read) | 同一事务两次读取同一行,期间被其他事务提交修改,结果不同 |
|
||||||
|
| 幻读(Phantom Read) | 同一事务两次执行相同范围查询,结果行数量因其他事务提交而变化 |
|
||||||
|
| 丢失更新(Lost Update) | 两个事务基于同一旧值更新,后提交的结果覆盖前一个结果 |
|
||||||
|
|
||||||
|
SQL标准定义四个主要隔离级别:
|
||||||
|
|
||||||
|
| 隔离级别 | 基本含义 | PostgreSQL说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `READ UNCOMMITTED` | 理论上允许读取未提交数据 | PostgreSQL内部按`READ COMMITTED`处理 |
|
||||||
|
| `READ COMMITTED` | 每条语句读取执行开始前已提交的数据 | PostgreSQL默认级别 |
|
||||||
|
| `REPEATABLE READ` | 事务内多次查询基于稳定快照 | 提交时仍可能出现并发冲突 |
|
||||||
|
| `SERIALIZABLE` | 尽量表现得像事务串行执行 | 冲突时可能要求应用重试事务 |
|
||||||
|
|
||||||
|
隔离级别不能代替所有业务并发控制。本课在默认`READ COMMITTED`下使用`SELECT ... FOR UPDATE`锁住即将修改的账户行。
|
||||||
|
|
||||||
|
## 七、JDBC与Psycopg事务对照
|
||||||
|
|
||||||
|
| Java常见写法 | Psycopg写法 | 含义 |
|
||||||
|
|---|---|---|
|
||||||
|
| `connection.setAutoCommit(false)` | 默认连接首次操作自动进入事务 | 开始事务工作 |
|
||||||
|
| `connection.commit()` | 正常离开连接`with` | 提交 |
|
||||||
|
| `connection.rollback()` | 异常离开连接`with` | 回滚 |
|
||||||
|
| `try-with-resources` | `with psycopg.connect(...)` | 管理连接生命周期 |
|
||||||
|
| Mapper/DAO | Repository | 封装SQL和结果转换 |
|
||||||
|
| Service | Service | 组织业务规则 |
|
||||||
|
| `@Transactional` | 外层连接/事务上下文 | 划定事务边界 |
|
||||||
|
|
||||||
|
Python没有Spring默认提供的声明式`@Transactional`。直接使用Psycopg时,需要显式组织事务上下文;后续SQLAlchemy和FastAPI课程会进一步统一Session生命周期。
|
||||||
|
|
||||||
|
## 八、Psycopg默认事务行为
|
||||||
|
|
||||||
|
Psycopg遵循DB-API习惯:连接默认不是自动提交模式。包括`SELECT`在内的数据库操作通常都会启动事务。
|
||||||
|
|
||||||
|
```python
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
```
|
||||||
|
|
||||||
|
正常离开`with`时提交;块内异常传播出去时回滚;最后关闭连接。
|
||||||
|
|
||||||
|
### 8.1 异常不能在事务内部被吞掉
|
||||||
|
|
||||||
|
错误写法:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
try:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
raise TransferError("后续失败")
|
||||||
|
except TransferError:
|
||||||
|
print("失败")
|
||||||
|
```
|
||||||
|
|
||||||
|
异常已经在`with`内部被捕获,连接上下文看到的是“正常结束”,可能提交前面的更新。
|
||||||
|
|
||||||
|
正确边界:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
raise TransferError("后续失败")
|
||||||
|
except TransferError as error:
|
||||||
|
print(error)
|
||||||
|
```
|
||||||
|
|
||||||
|
异常先离开`with`,触发回滚,然后才由外层处理。
|
||||||
|
|
||||||
|
### 8.2 手动提交和回滚
|
||||||
|
|
||||||
|
连接上下文适合“整个代码块就是一个事务”的情况,也可以显式控制:
|
||||||
|
|
||||||
|
```python
|
||||||
|
connection = psycopg.connect(**database_config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
connection.execute("UPDATE ...")
|
||||||
|
connection.commit()
|
||||||
|
except Exception:
|
||||||
|
connection.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
这与JDBC手动事务非常接近。但只要业务边界能自然表达为代码块,优先使用`with`,可以减少遗漏回滚或关闭连接的风险。
|
||||||
|
|
||||||
|
### 8.3 连接with与事务with的区别
|
||||||
|
|
||||||
|
本课使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
它同时管理事务和连接生命周期。Psycopg还提供:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with connection.transaction():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
后者只划定一个事务或保存点范围,不负责创建连接。它适合长连接或连接池场景。本阶段后续课程结合连接池时再深入使用。
|
||||||
|
|
||||||
|
## 九、Repository与Service职责
|
||||||
|
|
||||||
|
本课采用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
main
|
||||||
|
↓ 创建连接并划定事务
|
||||||
|
Service
|
||||||
|
↓ 组织转账规则
|
||||||
|
Repository
|
||||||
|
↓ 执行参数化SQL
|
||||||
|
PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
Repository不应在每个方法中调用`commit()`,否则扣款刚提交、入账却失败时,外层已经无法回滚整个业务操作。
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AccountRepository:
|
||||||
|
def __init__(self, connection):
|
||||||
|
self.connection = connection
|
||||||
|
|
||||||
|
def change_balance(self, account_no, amount):
|
||||||
|
self.connection.execute(...)
|
||||||
|
# 这里不提交。
|
||||||
|
```
|
||||||
|
|
||||||
|
Service也不负责创建连接,它复用同一个Repository,从而保证多个SQL处于同一事务。
|
||||||
|
|
||||||
|
## 十、FOR UPDATE、锁与死锁
|
||||||
|
|
||||||
|
转账前先查询余额:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT balance
|
||||||
|
FROM course_bank_account
|
||||||
|
WHERE account_no = %s
|
||||||
|
FOR UPDATE
|
||||||
|
```
|
||||||
|
|
||||||
|
`FOR UPDATE`会锁定选中的行,直到事务提交或回滚。它能防止两个并发事务同时读取相同旧余额后分别扣款。
|
||||||
|
|
||||||
|
这种“先锁定,再判断和修改”的做法属于悲观锁:代码假设并发冲突可能发生,因此提前取得排他性的行锁。
|
||||||
|
|
||||||
|
锁会持续到事务提交或回滚。事务范围过大,会让其他请求等待更久,因此事务中不应夹杂耗时的网络请求、人工操作或无关计算。
|
||||||
|
|
||||||
|
### 10.1 死锁
|
||||||
|
|
||||||
|
如果事务一先锁A再锁B,事务二同时先锁B再锁A,双方可能互相等待。数据库会检测死锁并中止其中一个事务。
|
||||||
|
|
||||||
|
降低死锁风险的常用办法:
|
||||||
|
|
||||||
|
- 多个事务按照统一顺序锁定资源,例如始终按账号升序;
|
||||||
|
- 缩短事务时间;
|
||||||
|
- 只锁真正需要修改的行;
|
||||||
|
- 应用捕获死锁或序列化失败,并按策略重试整个事务。
|
||||||
|
|
||||||
|
## 十一、批量操作
|
||||||
|
|
||||||
|
多条数据执行相同SQL时,可以使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.executemany(
|
||||||
|
"INSERT INTO course_bank_account VALUES (%s, %s, %s)",
|
||||||
|
accounts,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
它比手工拼接多条SQL安全,也明确表达“同一语句、不同参数”。它不等于无限制地一次提交海量数据;生产中仍要根据数据量分批。
|
||||||
|
|
||||||
|
## 十二、完整示例
|
||||||
|
|
||||||
|
示例文件为[transaction_repository_example.py](./transaction_repository_example.py),包含:
|
||||||
|
|
||||||
|
- `AccountRepository`:建表、初始化、查询和更新;
|
||||||
|
- `TransferService`:金额检查、余额检查、扣款和入账;
|
||||||
|
- 成功事务:转账200元并自动提交;
|
||||||
|
- 失败事务:先扣50元再抛出异常,验证自动回滚;
|
||||||
|
- 独立连接回查:证明提交和回滚的最终状态。
|
||||||
|
|
||||||
|
## 十三、准备配置
|
||||||
|
|
||||||
|
在第二课目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .\config.example.toml .\config.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
填写第一课使用的同一套专用练习数据库配置。`config.toml`已被项目`.gitignore`排除。
|
||||||
|
|
||||||
|
## 十四、运行方法与预期结果
|
||||||
|
|
||||||
|
激活已安装Psycopg的Conda环境:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
conda activate python-test
|
||||||
|
python .\transaction_repository_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
关键结果应为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
初始余额:
|
||||||
|
COURSE-A001|小明|余额:1000.00
|
||||||
|
COURSE-A002|小红|余额:500.00
|
||||||
|
成功转账 200 元后:
|
||||||
|
COURSE-A001|小明|余额:800.00
|
||||||
|
COURSE-A002|小红|余额:700.00
|
||||||
|
失败事务已回滚:模拟第二步失败,验证前一步更新会被回滚。
|
||||||
|
失败事务回滚后:
|
||||||
|
COURSE-A001|小明|余额:800.00
|
||||||
|
COURSE-A002|小红|余额:700.00
|
||||||
|
```
|
||||||
|
|
||||||
|
重复运行时,示例会先删除`COURSE-`前缀数据并重新初始化,因此结果保持一致。
|
||||||
|
|
||||||
|
## 十五、常见错误
|
||||||
|
|
||||||
|
### 15.1 Repository内部提交
|
||||||
|
|
||||||
|
这会破坏跨多个SQL的原子性。提交和回滚应由业务事务边界统一控制。
|
||||||
|
|
||||||
|
### 15.2 在with内部捕获业务异常
|
||||||
|
|
||||||
|
异常没有传播给连接上下文,可能导致错误提交。先让异常离开事务块,再在外层捕获。
|
||||||
|
|
||||||
|
### 15.3 失败后继续使用同一事务
|
||||||
|
|
||||||
|
PostgreSQL语句失败后,当前事务通常进入失败状态。在回滚前继续执行SQL,会收到`current transaction is aborted`一类错误。
|
||||||
|
|
||||||
|
### 15.4 使用float表示金额
|
||||||
|
|
||||||
|
二进制浮点数可能产生精度误差。课程金额使用`Decimal("200.00")`,数据库使用`NUMERIC(12, 2)`。
|
||||||
|
|
||||||
|
### 15.5 先查询再更新却没有锁
|
||||||
|
|
||||||
|
单用户测试可能正常,但并发时可能发生余额覆盖或超额扣款。本课使用`FOR UPDATE`锁定账户行。
|
||||||
|
|
||||||
|
### 15.6 清理范围过大
|
||||||
|
|
||||||
|
不要使用无条件`DELETE`或`TRUNCATE`。示例和练习只删除指定前缀的课程数据。
|
||||||
|
|
||||||
|
## 十六、课堂练习
|
||||||
|
|
||||||
|
打开[practice.py](./practice.py),实现独立的钱包转账练习。题目已经明确:
|
||||||
|
|
||||||
|
1. 配置读取;
|
||||||
|
2. Repository方法;
|
||||||
|
3. Service业务规则;
|
||||||
|
4. 批量初始化;
|
||||||
|
5. 成功事务;
|
||||||
|
6. 失败回滚;
|
||||||
|
7. 回查、输出和异常处理。
|
||||||
|
|
||||||
|
## 十七、本课小结
|
||||||
|
|
||||||
|
- 事务把多条数据库操作组织成一个工作单元;
|
||||||
|
- ACID分别是原子性、一致性、隔离性和持久性;
|
||||||
|
- 提交确认修改,回滚取消尚未提交的修改;
|
||||||
|
- PostgreSQL事务中的SQL失败后,通常必须先回滚才能继续;
|
||||||
|
- 隔离级别控制并发事务互相可见的范围;
|
||||||
|
- 事务边界应该覆盖完整业务操作;
|
||||||
|
- Repository封装SQL,但不擅自提交;
|
||||||
|
- Service组织业务规则,但复用外部连接;
|
||||||
|
- 异常必须先离开事务上下文才能触发自动回滚;
|
||||||
|
- `executemany()`适合相同SQL的多组参数;
|
||||||
|
- `FOR UPDATE`用于锁定即将修改的数据;
|
||||||
|
- 金额应使用`Decimal`与数据库`NUMERIC`。
|
||||||
|
|
||||||
|
## 十八、验收标准
|
||||||
|
|
||||||
|
- 能使用转账说明为什么需要事务;
|
||||||
|
- 能结合示例解释ACID四个特性;
|
||||||
|
- 能区分提交、回滚和自动提交;
|
||||||
|
- 能简要说明脏读、不可重复读、幻读和丢失更新;
|
||||||
|
- 能解释连接上下文何时提交、何时回滚;
|
||||||
|
- 能说明为什么Repository不能随意提交;
|
||||||
|
- 标准示例重复运行且结果一致;
|
||||||
|
- 练习的成功转账同时更新两个钱包;
|
||||||
|
- 模拟失败后第一条更新被完整回滚;
|
||||||
|
- SQL全部参数化;
|
||||||
|
- 只操作课程专用表和指定前缀数据;
|
||||||
|
- 未提交真实`config.toml`。
|
||||||
10
04_数据库/4_2_Python数据库事务与数据访问层/config.example.toml
Normal file
10
04_数据库/4_2_Python数据库事务与数据访问层/config.example.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 复制本文件并重命名为 config.toml,再填写本地练习数据库信息。
|
||||||
|
# config.toml 已加入项目 .gitignore,不会被 Git 跟踪。
|
||||||
|
|
||||||
|
[postgresql]
|
||||||
|
host = "数据库主机"
|
||||||
|
port = 5432
|
||||||
|
dbname = "数据库名"
|
||||||
|
user = "用户名"
|
||||||
|
password = "密码"
|
||||||
|
connect_timeout = 10
|
||||||
130
04_数据库/4_2_Python数据库事务与数据访问层/practice.py
Normal file
130
04_数据库/4_2_Python数据库事务与数据访问层/practice.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# 第 4-2 课练习:使用事务完成安全转账
|
||||||
|
#
|
||||||
|
# 本文件只提供题目,不包含导入、代码骨架、测试数据或参考答案。
|
||||||
|
# 本练习会创建 course_wallet_account 表,并只操作 PRACTICE- 前缀的数据。
|
||||||
|
# 请勿把表名改成现有业务表,也不要删除不属于本练习的数据。
|
||||||
|
|
||||||
|
|
||||||
|
# 第一部分:准备配置和异常
|
||||||
|
# 1. 导入 Decimal、Path、tomllib 和 psycopg。
|
||||||
|
# 2. 从 psycopg 导入 Connection。
|
||||||
|
# 3. 使用 Path(__file__).with_name("config.toml") 定义 CONFIG_PATH。
|
||||||
|
# 4. 定义 WalletError(Exception),类体只写 pass。
|
||||||
|
# 5. 实现 load_database_config(config_path),要求与第一课相同:
|
||||||
|
# - 文件不存在时抛出明确的 RuntimeError;
|
||||||
|
# - 使用 tomllib 读取 [postgresql];
|
||||||
|
# - 配置节不是字典时抛出明确的 RuntimeError;
|
||||||
|
# - 返回数据库配置字典。
|
||||||
|
|
||||||
|
# 第二部分:定义 WalletRepository
|
||||||
|
# 1. __init__(self, connection) 保存 Connection,但不在 Repository 中创建连接。
|
||||||
|
# 2. create_table() 执行以下建表逻辑:
|
||||||
|
# - 表名 course_wallet_account;
|
||||||
|
# - wallet_no VARCHAR(30) 主键;
|
||||||
|
# - owner_name VARCHAR(50) 非空;
|
||||||
|
# - balance NUMERIC(12, 2) 非空并且大于等于 0;
|
||||||
|
# - 使用 CREATE TABLE IF NOT EXISTS。
|
||||||
|
# 3. reset_practice_wallets() 只删除 wallet_no LIKE 'PRACTICE-%' 的记录,
|
||||||
|
# 模式字符串必须作为参数传入,不得拼接 SQL。
|
||||||
|
# 4. add_wallets(wallets) 使用 with connection.cursor() 创建游标,
|
||||||
|
# 再调用 cursor.executemany() 批量新增。
|
||||||
|
# 5. find_balance_for_update(wallet_no) 使用参数化 SQL 和 FOR UPDATE:
|
||||||
|
# - 找不到时抛出 WalletError("钱包不存在:{wallet_no}");
|
||||||
|
# - 找到时返回 Decimal 余额。
|
||||||
|
# 6. change_balance(wallet_no, amount) 使用 balance = balance + %s 更新余额;
|
||||||
|
# - cursor.rowcount 不等于 1 时抛出钱包不存在异常;
|
||||||
|
# - 本方法不调用 commit() 或 rollback()。
|
||||||
|
# 7. find_practice_wallets() 查询 PRACTICE- 前缀记录,按 wallet_no 排序并返回结果。
|
||||||
|
#
|
||||||
|
# Repository 边界提醒:
|
||||||
|
# - Repository 负责 SQL 和结果转换;
|
||||||
|
# - 不要在 add_wallets()、change_balance() 内提交事务;
|
||||||
|
# - 多个 Repository 操作需要由外层业务事务统一提交或回滚。
|
||||||
|
|
||||||
|
# 第三部分:定义 WalletTransferService
|
||||||
|
# 1. __init__(self, repository) 保存 WalletRepository。
|
||||||
|
# 2. transfer(self, source_no, target_no, amount) 按顺序执行:
|
||||||
|
# - amount <= 0 时抛出 WalletError("转账金额必须大于 0。");
|
||||||
|
# - 调用 find_balance_for_update(source_no),保存 source_balance;
|
||||||
|
# - 调用 find_balance_for_update(target_no),确认目标钱包存在并锁定;
|
||||||
|
# - source_balance < amount 时抛出 WalletError("钱包余额不足。");
|
||||||
|
# - 调用 change_balance(source_no, -amount);
|
||||||
|
# - 调用 change_balance(target_no, amount)。
|
||||||
|
# 3. transfer() 不调用 commit() 或 rollback()。
|
||||||
|
|
||||||
|
# 第四部分:准备练习数据
|
||||||
|
# 1. 定义 prepare_data(database_config)。
|
||||||
|
# 2. 使用 with psycopg.connect(**database_config) as connection 管理事务。
|
||||||
|
# 3. 创建 WalletRepository(connection)。
|
||||||
|
# 4. 依次调用 create_table() 和 reset_practice_wallets()。
|
||||||
|
# 5. 调用 add_wallets() 批量新增:
|
||||||
|
# - ("PRACTICE-W001", "张三", Decimal("800.00"))
|
||||||
|
# - ("PRACTICE-W002", "李四", Decimal("300.00"))
|
||||||
|
# 6. 正常离开 with,让初始化事务自动提交。
|
||||||
|
|
||||||
|
# 第五部分:完成成功事务
|
||||||
|
# 1. 定义 run_successful_transfer(database_config)。
|
||||||
|
# 2. 使用一个连接上下文创建 Repository 和 Service。
|
||||||
|
# 3. 调用:
|
||||||
|
# service.transfer("PRACTICE-W001", "PRACTICE-W002", Decimal("150.00"))
|
||||||
|
# 4. 正常离开 with,不要手工调用 commit()。
|
||||||
|
# 5. 成功后两个余额应分别为 650.00 和 450.00。
|
||||||
|
|
||||||
|
# 第六部分:完成失败事务并验证回滚
|
||||||
|
# 1. 定义 run_failed_transfer(database_config)。
|
||||||
|
# 2. 在 try 中创建连接上下文和 Repository。
|
||||||
|
# 3. 先调用:
|
||||||
|
# repository.change_balance("PRACTICE-W001", Decimal("-50.00"))
|
||||||
|
# 4. 紧接着抛出 WalletError("模拟入账失败。")。
|
||||||
|
# 5. 在连接上下文外捕获 WalletError,并输出:
|
||||||
|
# “失败事务已回滚:模拟入账失败。”
|
||||||
|
# 6. 不要在 except 前捕获并吞掉异常,否则连接上下文无法自动回滚。
|
||||||
|
# 7. 回查后余额必须仍为 650.00 和 450.00,而不是 600.00 和 450.00。
|
||||||
|
|
||||||
|
# 第七部分:查询与 main() 输出
|
||||||
|
# 1. 定义 query_wallets(database_config),使用独立连接查询并返回练习钱包。
|
||||||
|
# 2. 定义 print_wallets(title, wallets),按以下格式逐行输出:
|
||||||
|
# “PRACTICE-W001|张三|余额:800.00”。
|
||||||
|
# 3. main() 依次调用:
|
||||||
|
# - load_database_config(CONFIG_PATH);
|
||||||
|
# - prepare_data(database_config);
|
||||||
|
# - print_wallets("初始余额:", query_wallets(database_config));
|
||||||
|
# - run_successful_transfer(database_config);
|
||||||
|
# - print_wallets("成功转账 150 元后:", query_wallets(database_config));
|
||||||
|
# - run_failed_transfer(database_config);
|
||||||
|
# - print_wallets("失败事务回滚后:", query_wallets(database_config))。
|
||||||
|
# 4. 分别捕获配置异常和 psycopg.Error,并输出中文场景说明。
|
||||||
|
# 5. 添加程序入口判断并调用 main()。
|
||||||
|
# 预期关键输出:
|
||||||
|
# 初始余额:
|
||||||
|
# PRACTICE-W001|张三|余额:800.00
|
||||||
|
# PRACTICE-W002|李四|余额:300.00
|
||||||
|
# 成功转账 150 元后:
|
||||||
|
# PRACTICE-W001|张三|余额:650.00
|
||||||
|
# PRACTICE-W002|李四|余额:450.00
|
||||||
|
# 失败事务已回滚:模拟入账失败。
|
||||||
|
# 失败事务回滚后:
|
||||||
|
# PRACTICE-W001|张三|余额:650.00
|
||||||
|
# PRACTICE-W002|李四|余额:450.00
|
||||||
|
|
||||||
|
|
||||||
|
# 自查清单:
|
||||||
|
# 1. Repository 是否复用外部传入的 Connection?
|
||||||
|
# 2. Repository 和 Service 是否都没有自行提交事务?
|
||||||
|
# 3. 扣款和入账是否处于同一个连接上下文?
|
||||||
|
# 4. 失败异常是否离开连接 with 后才被捕获?
|
||||||
|
# 5. 是否使用 FOR UPDATE 锁定待修改账户?
|
||||||
|
# 6. 批量初始化是否使用 executemany()?
|
||||||
|
# 7. 所有值是否通过参数化查询传入?
|
||||||
|
# 8. 是否只清理 PRACTICE- 前缀的练习数据?
|
||||||
|
|
||||||
|
|
||||||
|
# 最终验收标准:
|
||||||
|
# 1. practice.py 通过语法检查并能重复运行;
|
||||||
|
# 2. 初始化、成功转账和失败回滚的余额符合预期;
|
||||||
|
# 3. 失败事务没有保留第一条扣款更新;
|
||||||
|
# 4. Repository 只负责数据访问,Service 负责业务规则;
|
||||||
|
# 5. 外层连接上下文控制事务提交和回滚;
|
||||||
|
# 6. SQL 全部参数化,不拼接业务数据;
|
||||||
|
# 7. 只操作本课专用表和 PRACTICE- 前缀数据;
|
||||||
|
# 8. config.toml 与真实连接信息没有进入 Git。
|
||||||
3
04_数据库/4_2_Python数据库事务与数据访问层/requirements.txt
Normal file
3
04_数据库/4_2_Python数据库事务与数据访问层/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 第二课继续使用 Psycopg 3,不新增第三方框架。
|
||||||
|
# 若使用 Conda,可以在课程环境中安装 psycopg 或 psycopg-c。
|
||||||
|
psycopg>=3,<4
|
||||||
214
04_数据库/4_2_Python数据库事务与数据访问层/transaction_repository_example.py
Normal file
214
04_数据库/4_2_Python数据库事务与数据访问层/transaction_repository_example.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
"""第 4-2 课示例:使用事务和 Repository 完成安全转账。"""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
from psycopg import Connection
|
||||||
|
from psycopg.rows import dict_row
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_PATH = Path(__file__).with_name("config.toml")
|
||||||
|
|
||||||
|
|
||||||
|
class TransferError(Exception):
|
||||||
|
"""表示转账过程中可以预期的业务失败。"""
|
||||||
|
|
||||||
|
|
||||||
|
def load_database_config(config_path: Path) -> dict[str, str | int]:
|
||||||
|
"""读取第二课本地 TOML 数据库配置。"""
|
||||||
|
if not config_path.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
"未找到 config.toml,请复制 config.example.toml 并填写练习数据库配置。"
|
||||||
|
)
|
||||||
|
|
||||||
|
with config_path.open("rb") as config_file:
|
||||||
|
config_data = tomllib.load(config_file)
|
||||||
|
|
||||||
|
database_config = config_data.get("postgresql")
|
||||||
|
if not isinstance(database_config, dict):
|
||||||
|
raise RuntimeError("config.toml 缺少 [postgresql] 配置节。")
|
||||||
|
|
||||||
|
return database_config
|
||||||
|
|
||||||
|
|
||||||
|
class AccountRepository:
|
||||||
|
"""封装账户表 SQL,但不自行提交或回滚事务。"""
|
||||||
|
|
||||||
|
def __init__(self, connection: Connection) -> None:
|
||||||
|
self.connection = connection
|
||||||
|
|
||||||
|
def create_table(self) -> None:
|
||||||
|
"""创建本课专用表;表已存在时保持不变。"""
|
||||||
|
self.connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS course_bank_account (
|
||||||
|
account_no VARCHAR(30) PRIMARY KEY,
|
||||||
|
owner_name VARCHAR(50) NOT NULL,
|
||||||
|
balance NUMERIC(12, 2) NOT NULL CHECK (balance >= 0)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def reset_course_accounts(self) -> None:
|
||||||
|
"""只清理 COURSE- 前缀的课程数据,避免影响其他记录。"""
|
||||||
|
self.connection.execute(
|
||||||
|
"DELETE FROM course_bank_account WHERE account_no LIKE %s",
|
||||||
|
("COURSE-%",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_accounts(self, accounts: list[tuple[str, str, Decimal]]) -> None:
|
||||||
|
"""使用 executemany() 批量新增课程账户。"""
|
||||||
|
# executemany() 是 Cursor 的方法,因此显式创建并关闭游标。
|
||||||
|
with self.connection.cursor() as cursor:
|
||||||
|
cursor.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO course_bank_account (account_no, owner_name, balance)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
""",
|
||||||
|
accounts,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_balance_for_update(self, account_no: str) -> Decimal:
|
||||||
|
"""查询并锁定账户,防止并发事务同时修改同一余额。"""
|
||||||
|
result = self.connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT balance
|
||||||
|
FROM course_bank_account
|
||||||
|
WHERE account_no = %s
|
||||||
|
FOR UPDATE
|
||||||
|
""",
|
||||||
|
(account_no,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
raise TransferError(f"账户不存在:{account_no}")
|
||||||
|
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
def change_balance(self, account_no: str, amount: Decimal) -> None:
|
||||||
|
"""使用数据库加法更新余额,并检查目标账户是否存在。"""
|
||||||
|
cursor = self.connection.execute(
|
||||||
|
"""
|
||||||
|
UPDATE course_bank_account
|
||||||
|
SET balance = balance + %s
|
||||||
|
WHERE account_no = %s
|
||||||
|
""",
|
||||||
|
(amount, account_no),
|
||||||
|
)
|
||||||
|
if cursor.rowcount != 1:
|
||||||
|
raise TransferError(f"账户不存在:{account_no}")
|
||||||
|
|
||||||
|
def find_course_accounts(self) -> list[dict[str, object]]:
|
||||||
|
"""按账号查询课程账户,并以字典行返回。"""
|
||||||
|
cursor = self.connection.cursor(row_factory=dict_row)
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT account_no, owner_name, balance
|
||||||
|
FROM course_bank_account
|
||||||
|
WHERE account_no LIKE %s
|
||||||
|
ORDER BY account_no
|
||||||
|
""",
|
||||||
|
("COURSE-%",),
|
||||||
|
)
|
||||||
|
return list(cursor.fetchall())
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TransferService:
|
||||||
|
"""组织转账业务规则;事务由调用它的连接上下文统一管理。"""
|
||||||
|
|
||||||
|
def __init__(self, repository: AccountRepository) -> None:
|
||||||
|
self.repository = repository
|
||||||
|
|
||||||
|
def transfer(self, source_no: str, target_no: str, amount: Decimal) -> None:
|
||||||
|
"""在同一事务中完成扣款与入账。"""
|
||||||
|
if amount <= 0:
|
||||||
|
raise TransferError("转账金额必须大于 0。")
|
||||||
|
|
||||||
|
source_balance = self.repository.get_balance_for_update(source_no)
|
||||||
|
self.repository.get_balance_for_update(target_no)
|
||||||
|
|
||||||
|
if source_balance < amount:
|
||||||
|
raise TransferError("账户余额不足。")
|
||||||
|
|
||||||
|
self.repository.change_balance(source_no, -amount)
|
||||||
|
self.repository.change_balance(target_no, amount)
|
||||||
|
|
||||||
|
|
||||||
|
def print_accounts(title: str, accounts: list[dict[str, object]]) -> None:
|
||||||
|
"""输出当前课程账户余额。"""
|
||||||
|
print(title)
|
||||||
|
for account in accounts:
|
||||||
|
print(
|
||||||
|
f"{account['account_no']}|{account['owner_name']}|"
|
||||||
|
f"余额:{account['balance']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_data(database_config: dict[str, str | int]) -> None:
|
||||||
|
"""创建专用表并重置本课固定数据。"""
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
repository = AccountRepository(connection)
|
||||||
|
repository.create_table()
|
||||||
|
repository.reset_course_accounts()
|
||||||
|
repository.add_accounts(
|
||||||
|
[
|
||||||
|
("COURSE-A001", "小明", Decimal("1000.00")),
|
||||||
|
("COURSE-A002", "小红", Decimal("500.00")),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_successful_transfer(database_config: dict[str, str | int]) -> None:
|
||||||
|
"""演示正常离开连接上下文时自动提交事务。"""
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
service = TransferService(AccountRepository(connection))
|
||||||
|
service.transfer("COURSE-A001", "COURSE-A002", Decimal("200.00"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_failed_transfer(database_config: dict[str, str | int]) -> None:
|
||||||
|
"""演示异常离开连接上下文时自动回滚整个事务。"""
|
||||||
|
try:
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
repository = AccountRepository(connection)
|
||||||
|
|
||||||
|
# 先执行一条成功更新,再主动触发业务异常。
|
||||||
|
# 外层 with 会回滚,因此这 50 元扣款不会保留下来。
|
||||||
|
repository.change_balance("COURSE-A001", Decimal("-50.00"))
|
||||||
|
raise TransferError("模拟第二步失败,验证前一步更新会被回滚。")
|
||||||
|
except TransferError as error:
|
||||||
|
print(f"失败事务已回滚:{error}")
|
||||||
|
|
||||||
|
|
||||||
|
def query_accounts(
|
||||||
|
database_config: dict[str, str | int],
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
"""使用独立连接回查已经提交的数据。"""
|
||||||
|
with psycopg.connect(**database_config) as connection:
|
||||||
|
return AccountRepository(connection).find_course_accounts()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""依次演示初始化、提交、回滚和回查。"""
|
||||||
|
try:
|
||||||
|
database_config = load_database_config(CONFIG_PATH)
|
||||||
|
prepare_data(database_config)
|
||||||
|
print_accounts("初始余额:", query_accounts(database_config))
|
||||||
|
|
||||||
|
run_successful_transfer(database_config)
|
||||||
|
print_accounts("成功转账 200 元后:", query_accounts(database_config))
|
||||||
|
|
||||||
|
run_failed_transfer(database_config)
|
||||||
|
print_accounts("失败事务回滚后:", query_accounts(database_config))
|
||||||
|
except (OSError, tomllib.TOMLDecodeError, RuntimeError) as error:
|
||||||
|
print(f"配置读取失败:{error}")
|
||||||
|
except psycopg.Error as error:
|
||||||
|
print(f"数据库访问失败:{error}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
80
README.md
80
README.md
@@ -69,16 +69,24 @@
|
|||||||
|
|
||||||
### 第四阶段:数据库编程
|
### 第四阶段:数据库编程
|
||||||
|
|
||||||
- 数据库和关系型数据库的基本概念;
|
本阶段采用精简路线。学习者已经具备数据库、SQL 和 Java 数据库开发基础,因此不再单独讲解关系型数据库、增删改查、关联查询、索引和事务等通用知识,而是重点学习 Python 数据库编程方式以及它与 JDBC、MyBatis、MyBatis-Plus、JPA/Hibernate 的差异。
|
||||||
- SQL 基础语法;
|
|
||||||
- 表、字段、主键和外键;
|
本阶段统一使用 PostgreSQL,不同时维护 MySQL 和 PostgreSQL 两套示例。涉及常见数据库差异时,通过补充说明对照 PostgreSQL 与 MySQL,不影响后续将知识迁移到 MySQL。
|
||||||
- 数据的新增、查询、修改和删除;
|
|
||||||
- 条件查询、排序、分组和关联查询;
|
本阶段共五课:
|
||||||
- 索引与事务基础;
|
|
||||||
- Python 连接 MySQL 或 PostgreSQL;
|
1. `4_1_PostgreSQL与Psycopg入门`:连接远程 PostgreSQL,认识 Python 数据库 API(Database API,DB-API),使用 Psycopg 3 执行参数化 SQL,并与 JDBC 的连接、语句和结果集进行对照;
|
||||||
- 参数化查询与 SQL 注入防护;
|
2. `4_2_Python数据库事务与数据访问层`:学习提交、回滚、异常处理、批量操作,以及数据访问对象(Data Access Object,DAO)和 Repository 分层;
|
||||||
- SQLAlchemy 对象关系映射;
|
3. `4_3_SQLAlchemy基础`:学习 SQLAlchemy 2.x 的 Engine、Session、声明式模型和基本增删改查,并与 JPA/Hibernate、MyBatis-Plus 进行对照;
|
||||||
- 数据库综合项目:持久化图书管理系统。
|
4. `4_4_SQLAlchemy关系映射与工程实践`:学习一对多、多对多、级联操作、加载策略、N+1 查询问题和业务分层;
|
||||||
|
5. `4_5_数据库综合项目`:使用 PostgreSQL 和 SQLAlchemy 2.x,把第三阶段的内存版图书管理系统升级为持久化版本。
|
||||||
|
|
||||||
|
本阶段采用两层数据库操作路线:
|
||||||
|
|
||||||
|
- Psycopg 3 对应 JDBC 驱动层,用于理解连接、游标、参数化 SQL、查询结果和事务;
|
||||||
|
- SQLAlchemy 2.x 提供 SQL 工具和对象关系映射(Object Relational Mapping,ORM)能力,其中 ORM 的定位更接近 JPA/Hibernate,并具备部分与 MyBatis-Plus 相似的常规增删改查体验。
|
||||||
|
|
||||||
|
数据库连接地址、用户名和密码通过本地 `config.toml` 提供,不写入环境变量。仓库只提交脱敏的 `config.example.toml`,真实配置文件必须由 `.gitignore` 排除。课程只使用专用练习数据库和受限账号,不连接生产数据库。
|
||||||
|
|
||||||
### 第五阶段:Web 开发基础
|
### 第五阶段:Web 开发基础
|
||||||
|
|
||||||
@@ -176,8 +184,27 @@ Python/
|
|||||||
│ ├── 1_2_变量与数据类型/
|
│ ├── 1_2_变量与数据类型/
|
||||||
│ └── 1_3_输入与输出/
|
│ └── 1_3_输入与输出/
|
||||||
├── 02_python进阶/
|
├── 02_python进阶/
|
||||||
|
│ ├── 2_1_模块与包/
|
||||||
|
│ ├── 2_2_文件与目录操作/
|
||||||
|
│ ├── 2_3_异常处理/
|
||||||
|
│ ├── 2_4_推导式与简化写法/
|
||||||
|
│ ├── 2_5_迭代器与生成器/
|
||||||
|
│ ├── 2_6_装饰器/
|
||||||
|
│ ├── 2_7_类型注解/
|
||||||
|
│ ├── 2_8_虚拟环境与依赖管理/
|
||||||
|
│ └── 2_9_python进阶综合项目/
|
||||||
├── 03_面向对象/
|
├── 03_面向对象/
|
||||||
|
│ ├── 3_1_Python与Java的类和对象/
|
||||||
|
│ ├── 3_2_Python与Java的封装差异/
|
||||||
|
│ ├── 3_3_Python与Java的继承和多态差异/
|
||||||
|
│ ├── 3_4_Python特殊方法与数据类/
|
||||||
|
│ └── 3_5_面向对象综合项目/
|
||||||
├── 04_数据库/
|
├── 04_数据库/
|
||||||
|
│ ├── 4_1_PostgreSQL与Psycopg入门/
|
||||||
|
│ ├── 4_2_Python数据库事务与数据访问层/
|
||||||
|
│ ├── 4_3_SQLAlchemy基础/
|
||||||
|
│ ├── 4_4_SQLAlchemy关系映射与工程实践/
|
||||||
|
│ └── 4_5_数据库综合项目/
|
||||||
├── 05_web基础/
|
├── 05_web基础/
|
||||||
├── 06_fastapi/
|
├── 06_fastapi/
|
||||||
├── 07_django/
|
├── 07_django/
|
||||||
@@ -188,13 +215,13 @@ Python/
|
|||||||
|
|
||||||
## 当前学习进度
|
## 当前学习进度
|
||||||
|
|
||||||
- 当前阶段:第二阶段——Python 进阶。
|
- 当前阶段:第四阶段——数据库编程。
|
||||||
- 当前课程:`2_2_文件与目录操作`。
|
- 当前课程:`4_2_Python数据库事务与数据访问层`。
|
||||||
- 当前状态:正在学习文件与目录操作。
|
- 当前状态:第四阶段前两课已完成,并通过 PostgreSQL 连接、成功提交和异常回滚验证。
|
||||||
- 已完成课程:`1_1_hello_world` 至 `1_14_python基础综合项目`,以及 `2_1_模块与包`。
|
- 已完成课程:第一阶段 `1_1_hello_world` 至 `1_14_python基础综合项目`、第二阶段 `2_1_模块与包` 至 `2_9_python进阶综合项目`、第三阶段 `3_1` 至 `3_5`,以及第四阶段 `4_1`、`4_2`。
|
||||||
- 学习中的课程:`2_2_文件与目录操作`。
|
- 学习中的课程:无。
|
||||||
- 已创建课程目录:第一阶段全部课程,以及 `02_python进阶/2_1_模块与包/`、`02_python进阶/2_2_文件与目录操作/`。
|
- 已创建课程目录:第一阶段全部课程、第二阶段全部课程、第三阶段 `3_1` 至 `3_5`,以及第四阶段 `4_1`、`4_2`。
|
||||||
- 下一步:完成文件与目录操作练习,然后学习异常处理。
|
- 下一步:进入 `4_3_SQLAlchemy基础`,学习 Engine、Session、声明式模型和 ORM 增删改查。
|
||||||
|
|
||||||
## 建议环境
|
## 建议环境
|
||||||
|
|
||||||
@@ -202,19 +229,22 @@ Python/
|
|||||||
- Python:学习开始时选择当前稳定版本;
|
- Python:学习开始时选择当前稳定版本;
|
||||||
- 编辑器:PyCharm 或 Visual Studio Code;
|
- 编辑器:PyCharm 或 Visual Studio Code;
|
||||||
- 命令行:PowerShell;
|
- 命令行:PowerShell;
|
||||||
- 数据库:前期可使用 SQLite,后期学习 MySQL 或 PostgreSQL;
|
- 数据库:第四阶段统一使用专用的远程 PostgreSQL 练习数据库;
|
||||||
|
- Python 数据库驱动:Psycopg 3;
|
||||||
|
- 对象关系映射工具:SQLAlchemy 2.x;
|
||||||
- 浏览器:Chrome、Edge 或其他现代浏览器。
|
- 浏览器:Chrome、Edge 或其他现代浏览器。
|
||||||
|
|
||||||
具体版本和安装步骤将在第一课中核对并讲解,避免因为版本变化使用过时的安装方式。
|
具体版本和安装步骤将在第一课中核对并讲解,避免因为版本变化使用过时的安装方式。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
上一课“模块与包”已经完成。当前正在学习文件与目录操作,本课将学习:
|
第三阶段已完成,已经掌握:
|
||||||
|
|
||||||
1. 文件、目录和路径的基本含义;
|
1. 使用 dataclass 建模图书和会员;
|
||||||
2. 使用 `Path` 表示和组合路径;
|
2. 使用普通业务类协调借书与还书;
|
||||||
3. 创建练习目录;
|
3. 通过组合管理对象关系;
|
||||||
4. 写入、追加和读取文本文件;
|
4. 使用自定义异常表达业务失败;
|
||||||
5. 检查路径并遍历目录内容。
|
5. 使用鸭子类型接入通知器;
|
||||||
|
6. 验证正常流程和主要异常流程。
|
||||||
|
|
||||||
完成本课练习并确认继续后,下一课将学习异常处理。
|
第四阶段前两课已经完成,能够使用本地 TOML 配置、Psycopg 3 和参数化 SQL 安全访问远程 PostgreSQL,并通过转账场景掌握事务提交、异常回滚、批量操作以及 Repository/Service 分层。下一步进入 SQLAlchemy 2.x 基础。
|
||||||
|
|||||||
Reference in New Issue
Block a user