feat(python进阶): 完成第三至第八课教学内容
This commit is contained in:
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. 是否保留了完整题目和验收说明。
|
||||
Reference in New Issue
Block a user