9.1 KiB
第 2-6 课:装饰器
一、本课目标
完成本课后,你将能够:
- 理解函数可以像其他值一样赋值和传递;
- 解释装饰器用于解决什么问题;
- 编写接收函数并返回新函数的简单装饰器;
- 使用
@装饰器名称增强函数; - 使用
*args和**kwargs转发参数; - 保留被装饰函数的返回值;
- 使用
functools.wraps保留原函数信息。
二、前置知识
学习本课前,需要掌握:
- 定义和调用函数;
- 参数、返回值和局部变量;
*args与**kwargs;- 条件判断;
- 模块导入。
三、为什么需要装饰器
多个函数可能需要执行相同的额外操作,例如:
- 调用前记录日志;
- 检查用户是否有权限;
- 检查 Agent 是否启用;
- 统计函数执行时间;
- 统一处理函数调用结果。
如果把相同代码复制到每个函数中,修改时容易遗漏。装饰器(Decorator)可以在不修改原函数主体的情况下,为函数统一增加行为。
本课使用“记录调用日志”和“检查 Agent 状态”两个简单场景。
四、函数也可以作为值
定义函数后,函数名不加括号表示函数本身:
def greet_agent(name):
return f"你好,{name}。"
greeting_function = greet_agent
result = greeting_function("代码助手")
print(result)
这里没有调用 greet_agent,而是把函数交给另一个变量。随后可以通过新变量调用同一个函数。
注意区别:
greeting_function = greet_agent # 保存函数本身
greeting_result = greet_agent("代码助手") # 立即调用并保存结果
装饰器能够工作,正是因为函数可以作为值传入另一个函数,也可以从另一个函数返回。
五、函数内部可以定义函数
Python 允许在一个函数中定义另一个函数:
def outer():
def inner():
return "内部函数的结果"
return inner
outer() 返回的是 inner 函数本身,而不是它的调用结果:
inner_function = outer()
result = inner_function()
装饰器通常会在内部定义一个名为 wrapper 的函数。wrapper 的中文含义是“包装器”,它会包住原函数的调用过程。
六、第一个装饰器
def log_call(func):
def wrapper():
print("函数开始执行。")
result = func()
print("函数执行结束。")
return result
return wrapper
逐层理解:
log_call(func)接收需要增强的原函数;- 内部定义
wrapper(); wrapper()先执行额外逻辑;func()调用原函数;- 保存并返回原函数结果;
log_call()最后返回wrapper函数本身。
这里必须写:
return wrapper
不要写成 return wrapper(),后者会立即调用包装函数。
七、手动使用装饰器
假设有一个函数:
def get_agent_count():
return 3
可以手动包装:
get_agent_count = log_call(get_agent_count)
等号右边把原函数传给装饰器,装饰器返回 wrapper;等号左边再让原名称指向包装后的函数。
以后调用 get_agent_count(),实际先进入 wrapper(),再由它调用原函数。
八、使用 @ 语法
Python 提供了更清楚的写法:
@log_call
def get_agent_count():
return 3
它与下面的手动写法表达相同含义:
get_agent_count = log_call(get_agent_count)
@log_call 必须紧挨着函数定义的上一行。
九、转发不同参数
如果 wrapper() 不接收参数,被装饰函数也就无法正常接收参数。为了适应不同函数,可以使用之前学过的 *args 和 **kwargs:
def log_call(func):
def wrapper(*args, **kwargs):
print(f"调用函数:{func.__name__}")
result = func(*args, **kwargs)
return result
return wrapper
*args收集并转发位置参数;**kwargs收集并转发关键字参数;func.__name__是原函数名称。
这使装饰器可以包装参数数量不同的函数。
十、不要丢失原函数返回值
错误写法:
def wrapper(*args, **kwargs):
func(*args, **kwargs)
原函数虽然被调用,但结果没有返回,调用者最终得到 None。
正确过程:
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
装饰器可以增加行为,但不应无意中改变原函数正常的返回结果。
十一、使用 functools.wraps
包装后,函数名称默认可能变成 wrapper。这会影响调试、日志和帮助信息。
Python 标准库的 functools 模块提供 wraps:
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@wraps(func) 会帮助包装函数保留原函数的名称和说明文字。它本身也是装饰器;本课先掌握固定写法。
十二、调用前进行条件检查
装饰器可以决定是否调用原函数:
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 提前返回,原函数不会执行;如果已启用,参数会继续转交给原函数。
十三、完整示例
示例文件:
02_python进阶/2_6_装饰器/decorator_example.py
示例演示:
- 使用
@log_operation统一增加开始和结束日志; - 正确转发 Agent 参数;
- 保留工具数量返回值;
- 使用
@require_enabled阻止停用 Agent 执行任务; - 使用
@wraps保留原函数名称。
十四、运行方法
在项目根目录运行示例:
python .\02_python进阶\2_6_装饰器\decorator_example.py
完成练习后运行:
python .\02_python进阶\2_6_装饰器\practice.py
十五、预期结果
一、统一记录操作日志
开始执行:count_tools
执行结束:count_tools
工具数量:2
==============================
二、执行前检查 Agent 状态
聊天助手 当前已停用,无法执行操作。
启用 Agent 的结果:代码助手 正在执行任务:检查代码
停用 Agent 的结果:None
==============================
三、检查函数名称
被装饰后的函数名:count_tools
十六、常见错误
16.1 把函数赋值写成函数调用
saved_function = greet_agent # 保存函数
saved_result = greet_agent("代码助手") # 保存结果
括号会立即调用函数,两种写法含义不同。
16.2 返回 wrapper()
装饰器最后应返回函数本身:
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,依次完成:
- 把函数本身赋值给另一个变量;
- 编写并使用日志装饰器;
- 检查
@wraps是否保留函数名称; - 编写 Agent 启用状态装饰器;
- 验证停用 Agent 不会执行原函数;
- 在
main()中输出正常和拦截结果。
十八、参考答案
参考答案暂不写入练习文件。完成后,我会检查装饰器是否真正调用原函数、是否保留参数和返回值,以及停用分支是否正确阻止原函数执行。
十九、本课小结
- 函数可以赋值给变量,也可以传入和返回;
- 装饰器接收原函数并返回包装函数;
@decorator是手动重新赋值的简化语法;wrapper在调用原函数前后增加统一行为;*args和**kwargs可以转发不同参数;- 包装函数应保留原函数返回值;
@wraps(func)可以保留原函数名称等信息;- 装饰器可以在调用前检查条件并阻止无效操作。
二十、验收标准
- 能解释函数名加括号与不加括号的区别;
- 能说明装饰器接收和返回的内容;
- 能手动完成一次函数包装;
- 能使用
@语法应用装饰器; wrapper能转发位置参数和关键字参数;- 原函数返回值不会丢失;
@wraps能保留原函数名称;- 启用 Agent 可以执行任务;
- 停用 Agent 会被阻止并返回
None; - 装饰器只负责通用附加行为,原函数保留具体业务逻辑。