feat(python进阶): 完成第三至第八课教学内容

This commit is contained in:
zhiye.sun
2026-08-07 17:14:48 +08:00
parent f7c820b0f4
commit aff05283a2
21 changed files with 3504 additions and 13 deletions

View 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`
- 装饰器只负责通用附加行为,原函数保留具体业务逻辑。

View 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()

View 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. 是否保留了完整题目和验收说明。