Files

9.1 KiB
Raw Permalink Blame History

第 2-6 课:装饰器

一、本课目标

完成本课后,你将能够:

  1. 理解函数可以像其他值一样赋值和传递;
  2. 解释装饰器用于解决什么问题;
  3. 编写接收函数并返回新函数的简单装饰器;
  4. 使用 @装饰器名称 增强函数;
  5. 使用 *args**kwargs 转发参数;
  6. 保留被装饰函数的返回值;
  7. 使用 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

逐层理解:

  1. log_call(func) 接收需要增强的原函数;
  2. 内部定义 wrapper()
  3. wrapper() 先执行额外逻辑;
  4. func() 调用原函数;
  5. 保存并返回原函数结果;
  6. 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

示例演示:

  1. 使用 @log_operation 统一增加开始和结束日志;
  2. 正确转发 Agent 参数;
  3. 保留工具数量返回值;
  4. 使用 @require_enabled 阻止停用 Agent 执行任务;
  5. 使用 @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,依次完成:

  1. 把函数本身赋值给另一个变量;
  2. 编写并使用日志装饰器;
  3. 检查 @wraps 是否保留函数名称;
  4. 编写 Agent 启用状态装饰器;
  5. 验证停用 Agent 不会执行原函数;
  6. main() 中输出正常和拦截结果。

十八、参考答案

参考答案暂不写入练习文件。完成后,我会检查装饰器是否真正调用原函数、是否保留参数和返回值,以及停用分支是否正确阻止原函数执行。

十九、本课小结

  • 函数可以赋值给变量,也可以传入和返回;
  • 装饰器接收原函数并返回包装函数;
  • @decorator 是手动重新赋值的简化语法;
  • wrapper 在调用原函数前后增加统一行为;
  • *args**kwargs 可以转发不同参数;
  • 包装函数应保留原函数返回值;
  • @wraps(func) 可以保留原函数名称等信息;
  • 装饰器可以在调用前检查条件并阻止无效操作。

二十、验收标准

  • 能解释函数名加括号与不加括号的区别;
  • 能说明装饰器接收和返回的内容;
  • 能手动完成一次函数包装;
  • 能使用 @ 语法应用装饰器;
  • wrapper 能转发位置参数和关键字参数;
  • 原函数返回值不会丢失;
  • @wraps 能保留原函数名称;
  • 启用 Agent 可以执行任务;
  • 停用 Agent 会被阻止并返回 None
  • 装饰器只负责通用附加行为,原函数保留具体业务逻辑。