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