Files
PythonLearn/02_python进阶/2_6_装饰器/README.md

358 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第 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`
- 装饰器只负责通用附加行为,原函数保留具体业务逻辑。