From aff05283a2aa41951f6f11080d1826655b3c0029 Mon Sep 17 00:00:00 2001 From: "zhiye.sun" Date: Fri, 7 Aug 2026 17:14:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(python=E8=BF=9B=E9=98=B6):=20=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E7=AC=AC=E4=B8=89=E8=87=B3=E7=AC=AC=E5=85=AB=E8=AF=BE?= =?UTF-8?q?=E6=95=99=E5=AD=A6=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 02_python进阶/2_3_异常处理/.gitignore | 2 + 02_python进阶/2_3_异常处理/README.md | 341 +++++++++++++++ .../2_3_异常处理/exception_example.py | 90 ++++ 02_python进阶/2_3_异常处理/practice.py | 58 +++ 02_python进阶/2_4_推导式与简化写法/README.md | 355 +++++++++++++++ .../comprehension_example.py | 77 ++++ .../2_4_推导式与简化写法/practice.py | 65 +++ 02_python进阶/2_5_迭代器与生成器/README.md | 371 ++++++++++++++++ .../iterator_generator_example.py | 74 ++++ 02_python进阶/2_5_迭代器与生成器/practice.py | 73 ++++ 02_python进阶/2_6_装饰器/README.md | 357 +++++++++++++++ 02_python进阶/2_6_装饰器/decorator_example.py | 79 ++++ 02_python进阶/2_6_装饰器/practice.py | 83 ++++ 02_python进阶/2_7_类型注解/README.md | 383 ++++++++++++++++ 02_python进阶/2_7_类型注解/practice.py | 84 ++++ .../2_7_类型注解/type_annotation_example.py | 53 +++ .../2_8_虚拟环境与依赖管理/README.md | 410 ++++++++++++++++++ .../environment_example.py | 45 ++ .../2_8_虚拟环境与依赖管理/practice.py | 78 ++++ .../补充_conda_pip_uv与虚拟环境的区别.md | 405 +++++++++++++++++ README.md | 34 +- 21 files changed, 3504 insertions(+), 13 deletions(-) create mode 100644 02_python进阶/2_3_异常处理/.gitignore create mode 100644 02_python进阶/2_3_异常处理/README.md create mode 100644 02_python进阶/2_3_异常处理/exception_example.py create mode 100644 02_python进阶/2_3_异常处理/practice.py create mode 100644 02_python进阶/2_4_推导式与简化写法/README.md create mode 100644 02_python进阶/2_4_推导式与简化写法/comprehension_example.py create mode 100644 02_python进阶/2_4_推导式与简化写法/practice.py create mode 100644 02_python进阶/2_5_迭代器与生成器/README.md create mode 100644 02_python进阶/2_5_迭代器与生成器/iterator_generator_example.py create mode 100644 02_python进阶/2_5_迭代器与生成器/practice.py create mode 100644 02_python进阶/2_6_装饰器/README.md create mode 100644 02_python进阶/2_6_装饰器/decorator_example.py create mode 100644 02_python进阶/2_6_装饰器/practice.py create mode 100644 02_python进阶/2_7_类型注解/README.md create mode 100644 02_python进阶/2_7_类型注解/practice.py create mode 100644 02_python进阶/2_7_类型注解/type_annotation_example.py create mode 100644 02_python进阶/2_8_虚拟环境与依赖管理/README.md create mode 100644 02_python进阶/2_8_虚拟环境与依赖管理/environment_example.py create mode 100644 02_python进阶/2_8_虚拟环境与依赖管理/practice.py create mode 100644 02_python进阶/2_8_虚拟环境与依赖管理/补充_conda_pip_uv与虚拟环境的区别.md diff --git a/02_python进阶/2_3_异常处理/.gitignore b/02_python进阶/2_3_异常处理/.gitignore new file mode 100644 index 0000000..2b07365 --- /dev/null +++ b/02_python进阶/2_3_异常处理/.gitignore @@ -0,0 +1,2 @@ +example_data/ +practice_data/ diff --git a/02_python进阶/2_3_异常处理/README.md b/02_python进阶/2_3_异常处理/README.md new file mode 100644 index 0000000..0ce7e5a --- /dev/null +++ b/02_python进阶/2_3_异常处理/README.md @@ -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` 隐藏异常。 diff --git a/02_python进阶/2_3_异常处理/exception_example.py b/02_python进阶/2_3_异常处理/exception_example.py new file mode 100644 index 0000000..5c5e384 --- /dev/null +++ b/02_python进阶/2_3_异常处理/exception_example.py @@ -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() diff --git a/02_python进阶/2_3_异常处理/practice.py b/02_python进阶/2_3_异常处理/practice.py new file mode 100644 index 0000000..c7056fe --- /dev/null +++ b/02_python进阶/2_3_异常处理/practice.py @@ -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. 是否没有删除题目和验收说明。 diff --git a/02_python进阶/2_4_推导式与简化写法/README.md b/02_python进阶/2_4_推导式与简化写法/README.md new file mode 100644 index 0000000..6a13ea7 --- /dev/null +++ b/02_python进阶/2_4_推导式与简化写法/README.md @@ -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 列表; +- 没有为了简短而编写难以阅读的复杂表达式。 diff --git a/02_python进阶/2_4_推导式与简化写法/comprehension_example.py b/02_python进阶/2_4_推导式与简化写法/comprehension_example.py new file mode 100644 index 0000000..1afce97 --- /dev/null +++ b/02_python进阶/2_4_推导式与简化写法/comprehension_example.py @@ -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() diff --git a/02_python进阶/2_4_推导式与简化写法/practice.py b/02_python进阶/2_4_推导式与简化写法/practice.py new file mode 100644 index 0000000..c766574 --- /dev/null +++ b/02_python进阶/2_4_推导式与简化写法/practice.py @@ -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. 是否保留了完整题目和验收说明。 diff --git a/02_python进阶/2_5_迭代器与生成器/README.md b/02_python进阶/2_5_迭代器与生成器/README.md new file mode 100644 index 0000000..f612704 --- /dev/null +++ b/02_python进阶/2_5_迭代器与生成器/README.md @@ -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 数据。 diff --git a/02_python进阶/2_5_迭代器与生成器/iterator_generator_example.py b/02_python进阶/2_5_迭代器与生成器/iterator_generator_example.py new file mode 100644 index 0000000..6b3620e --- /dev/null +++ b/02_python进阶/2_5_迭代器与生成器/iterator_generator_example.py @@ -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() diff --git a/02_python进阶/2_5_迭代器与生成器/practice.py b/02_python进阶/2_5_迭代器与生成器/practice.py new file mode 100644 index 0000000..23e5ecb --- /dev/null +++ b/02_python进阶/2_5_迭代器与生成器/practice.py @@ -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. 是否保留了完整题目和验收说明。 diff --git a/02_python进阶/2_6_装饰器/README.md b/02_python进阶/2_6_装饰器/README.md new file mode 100644 index 0000000..48e4971 --- /dev/null +++ b/02_python进阶/2_6_装饰器/README.md @@ -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`; +- 装饰器只负责通用附加行为,原函数保留具体业务逻辑。 diff --git a/02_python进阶/2_6_装饰器/decorator_example.py b/02_python进阶/2_6_装饰器/decorator_example.py new file mode 100644 index 0000000..0913f12 --- /dev/null +++ b/02_python进阶/2_6_装饰器/decorator_example.py @@ -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() diff --git a/02_python进阶/2_6_装饰器/practice.py b/02_python进阶/2_6_装饰器/practice.py new file mode 100644 index 0000000..0c90d76 --- /dev/null +++ b/02_python进阶/2_6_装饰器/practice.py @@ -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. 是否保留了完整题目和验收说明。 diff --git a/02_python进阶/2_7_类型注解/README.md b/02_python进阶/2_7_类型注解/README.md new file mode 100644 index 0000000..39d8107 --- /dev/null +++ b/02_python进阶/2_7_类型注解/README.md @@ -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` 标注输出函数; +- 能说明注解与类型转换的区别; +- 能说明类型注解通常不会自动强制检查运行时数据; +- 所有注解与函数真实返回值保持一致。 diff --git a/02_python进阶/2_7_类型注解/practice.py b/02_python进阶/2_7_类型注解/practice.py new file mode 100644 index 0000000..cec492e --- /dev/null +++ b/02_python进阶/2_7_类型注解/practice.py @@ -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. 是否保留了完整题目和验收说明。 diff --git a/02_python进阶/2_7_类型注解/type_annotation_example.py b/02_python进阶/2_7_类型注解/type_annotation_example.py new file mode 100644 index 0000000..4f0290c --- /dev/null +++ b/02_python进阶/2_7_类型注解/type_annotation_example.py @@ -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() diff --git a/02_python进阶/2_8_虚拟环境与依赖管理/README.md b/02_python进阶/2_8_虚拟环境与依赖管理/README.md new file mode 100644 index 0000000..0fef818 --- /dev/null +++ b/02_python进阶/2_8_虚拟环境与依赖管理/README.md @@ -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` 中显示不同判断; +- 没有把个人绝对路径或虚假依赖写入项目文件。 diff --git a/02_python进阶/2_8_虚拟环境与依赖管理/environment_example.py b/02_python进阶/2_8_虚拟环境与依赖管理/environment_example.py new file mode 100644 index 0000000..b2948bd --- /dev/null +++ b/02_python进阶/2_8_虚拟环境与依赖管理/environment_example.py @@ -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() diff --git a/02_python进阶/2_8_虚拟环境与依赖管理/practice.py b/02_python进阶/2_8_虚拟环境与依赖管理/practice.py new file mode 100644 index 0000000..c37209d --- /dev/null +++ b/02_python进阶/2_8_虚拟环境与依赖管理/practice.py @@ -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. 是否保留了完整题目和验收说明。 diff --git a/02_python进阶/2_8_虚拟环境与依赖管理/补充_conda_pip_uv与虚拟环境的区别.md b/02_python进阶/2_8_虚拟环境与依赖管理/补充_conda_pip_uv与虚拟环境的区别.md new file mode 100644 index 0000000..7b9937a --- /dev/null +++ b/02_python进阶/2_8_虚拟环境与依赖管理/补充_conda_pip_uv与虚拟环境的区别.md @@ -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/) diff --git a/README.md b/README.md index 84d3372..4a1be0b 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,14 @@ Python/ │ ├── 1_2_变量与数据类型/ │ └── 1_3_输入与输出/ ├── 02_python进阶/ +│ ├── 2_1_模块与包/ +│ ├── 2_2_文件与目录操作/ +│ ├── 2_3_异常处理/ +│ ├── 2_4_推导式与简化写法/ +│ ├── 2_5_迭代器与生成器/ +│ ├── 2_6_装饰器/ +│ ├── 2_7_类型注解/ +│ └── 2_8_虚拟环境与依赖管理/ ├── 03_面向对象/ ├── 04_数据库/ ├── 05_web基础/ @@ -189,12 +197,12 @@ Python/ ## 当前学习进度 - 当前阶段:第二阶段——Python 进阶。 -- 当前课程:`2_2_文件与目录操作`。 -- 当前状态:正在学习文件与目录操作。 -- 已完成课程:`1_1_hello_world` 至 `1_14_python基础综合项目`,以及 `2_1_模块与包`。 -- 学习中的课程:`2_2_文件与目录操作`。 -- 已创建课程目录:第一阶段全部课程,以及 `02_python进阶/2_1_模块与包/`、`02_python进阶/2_2_文件与目录操作/`。 -- 下一步:完成文件与目录操作练习,然后学习异常处理。 +- 当前课程:第二阶段综合项目准备。 +- 当前状态:`2_8_虚拟环境与依赖管理` 已按学习者要求跳过并标记为完成,同时已补充 Conda、pip、uv 对比文档。 +- 已完成课程:`1_1_hello_world` 至 `1_14_python基础综合项目`,以及第二阶段 `2_1_模块与包` 至 `2_8_虚拟环境与依赖管理`。 +- 学习中的课程:暂无。 +- 已创建课程目录:第一阶段全部课程,以及第二阶段 `2_1_模块与包` 至 `2_8_虚拟环境与依赖管理`。 +- 下一步:进入 `2_9_python进阶综合项目`。 ## 建议环境 @@ -209,12 +217,12 @@ Python/ ## 下一步 -上一课“模块与包”已经完成。当前正在学习文件与目录操作,本课将学习: +第二阶段第 8 课已按学习者要求跳过并标记为完成,同时保留补充资料供以后查阅。下一步将进入综合项目,综合使用: -1. 文件、目录和路径的基本含义; -2. 使用 `Path` 表示和组合路径; -3. 创建练习目录; -4. 写入、追加和读取文本文件; -5. 检查路径并遍历目录内容。 +1. 模块与包; +2. 文件读写与异常处理; +3. 推导式、生成器和类型注解; +4. 使用多个文件组织任务管理程序; +5. 保存并恢复任务数据。 -完成本课练习并确认继续后,下一课将学习异常处理。 +确认继续后,将创建 `2_9_python进阶综合项目`。