Python装饰器如何带参数?

99ANYc3cd6
预计阅读时长 20 分钟
位置: 首页 参数 正文

不带参数的装饰器

我们快速回顾一下不带参数的装饰器是如何工作的,它的核心作用是在不修改原函数代码的情况下,为函数增加额外的功能。

python 装饰器 带参数
(图片来源网络,侵删)

一个简单的装饰器 my_decorator

def my_decorator(func):
    """一个简单的装饰器"""
    def wrapper():
        print("函数执行前的操作")
        func()  # 调用原始函数
        print("函数执行后的操作")
    return wrapper
@my_decorator
def say_hello():
    print("Hello, World!")
# 调用 say_hello 实际上是调用 wrapper
say_hello()

输出:

函数执行前的操作
Hello, World!
函数执行后的操作

工作原理:

  1. 当 Python 解释器看到 @my_decorator 时,它相当于执行了 say_hello = my_decorator(say_hello)
  2. my_decorator 函数接收 say_hello 函数作为参数,并返回一个新的函数 wrapper
  3. 之后,say_hello 这个名字就指向了 wrapper 函数。

带参数的装饰器:为什么需要?

不带参数的装饰器很好用,但它的功能是固定的,上面的装饰器只能打印固定的两句话,如果我们想让它更灵活,比如可以自定义打印的消息,该怎么办?

python 装饰器 带参数
(图片来源网络,侵删)

这时,带参数的装饰器就派上用场了,它允许我们在使用装饰器时传递参数,从而让装饰器的行为可以动态配置。


如何实现带参数的装饰器?

实现带参数的装饰器,关键在于增加一个函数层级

一个带参数的装饰器,本质上是一个返回装饰器函数的函数,听起来有点绕,我们把它拆解开来:

  • 最外层函数:接收装饰器的参数。
  • 中间层函数:接收原始函数,并返回一个新的包装函数。
  • 最内层函数:包装函数,包含原始函数的调用和额外的逻辑。

我们用一个例子来改造上面的 say_hello 函数,让装饰器可以接收自定义的 before_msgafter_msg

python 装饰器 带参数
(图片来源网络,侵删)
# 1. 最外层函数,接收装饰器的参数
def decorator_with_args(before_msg, after_msg):
    """这个函数返回一个真正的装饰器"""
    print(f"装饰器工厂被调用,参数: before_msg='{before_msg}', after_msg='{after_msg}'")
    # 2. 中间层函数,接收原始函数 func
    def actual_decorator(func):
        """这是一个标准的装饰器,它包装 func"""
        # 3. 最内层函数,包装函数,包含核心逻辑
        def wrapper(*args, **kwargs):
            print(before_msg)
            result = func(*args, **kwargs)  # 调用原始函数,并传递参数
            print(after_msg)
            return result # 返回原始函数的返回值
        return wrapper
    return actual_decorator
# 使用带参数的装饰器
@decorator_with_args("准备打招呼...", "打招呼完成!")
def say_hello(name):
    print(f"Hello, {name}!")
    return f"Successfully greeted {name}."
# 调用被装饰的函数
print("\n--- 调用 say_hello ---")
result = say_hello("Alice")
print(f"函数返回值: {result}")

输出:

装饰器工厂被调用,参数: before_msg='准备打招呼...', after_msg='打招呼完成!'
--- 调用 say_hello ---
准备打招呼...
Hello, Alice!
打招呼完成!
函数返回值: Successfully greeted Alice.

代码解析:

  1. @decorator_with_args("准备打招呼...", "打招呼完成!") 的执行过程:

    • Python 首先调用 decorator_with_args("准备打招呼...", "打招呼完成!")
    • 这个函数会打印第一行日志,然后返回 actual_decorator 这个函数。
    • 语句实际上等价于 say_hello = actual_decorator(say_hello)
  2. actual_decorator(func) 的执行过程:

    • actual_decorator 接收 say_hello 作为参数 func
    • 它内部定义了 wrapper 函数,并返回这个 wrapper
    • say_hello 这个名字最终指向了 wrapper 函数。
  3. *`wrapper(args, kwargs)` 的执行过程:

    • 当我们调用 say_hello("Alice") 时,实际上是在调用 wrapper("Alice")
    • *args, **kwargs 使得 wrapper 可以接收任意数量和类型的参数,并将其传递给原始的 func
    • 它执行 before_msg,然后调用 func("Alice")(即 say_hello("Alice")),再执行 after_msg
    • 它返回 func 的执行结果,使得调用链的返回值得以保留。

更通用的带参数装饰器:functools.wraps

当你使用装饰器时,原函数的元信息(如函数名 __name__、文档字符串 __doc__ 等)会丢失,因为它们被 wrapper 函数的信息覆盖了。

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper
@my_decorator
def greet():
    """这是一个文档字符串,说明函数的功能。"""
    print("Hello!")
print(greet.__name__)  # 输出: wrapper
print(greet.__doc__)   # 输出: None

为了解决这个问题,Python 标准库 functools 提供了一个 wraps 装饰器,它专门用来修复被包装函数的元信息。

最佳实践: 在任何自定义装饰器的 wrapper 函数上都使用 @functools.wraps(func)

import functools
def my_decorator(func):
    @functools.wraps(func)  # 使用 @functools.wraps
    def wrapper(*args, **kwargs):
        """wrapper 函数的文档字符串"""
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper
@my_decorator
def greet():
    """这是一个文档字符串,说明函数的功能。"""
    print("Hello!")
print(greet.__name__)  # 输出: greet
print(greet.__doc__)   # 输出: 这是一个文档字符串,说明函数的功能。

我们把 @functools.wraps 应用到我们带参数的装饰器中,使其成为完整、规范的版本。

import functools
def log_execution(log_level):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{log_level}] 函数 {func.__name__} 即将执行...")
            result = func(*args, **kwargs)
            print(f"[{log_level}] 函数 {func.__name__} 执行完毕。")
            return result
        return wrapper
    return decorator
@log_execution("INFO")
def add(a, b):
    """将两个数字相加"""
    return a + b
print(f"计算结果: {add(3, 5)}")
print(f"函数文档: {add.__doc__}")

输出:

[INFO] 函数 add 即将执行...
[INFO] 函数 add 执行完毕。
计算结果: 8
函数文档: 将两个数字相加

类型 结构 工作原理 示例
无参数装饰器 def decorator(func): ... func = decorator(func) @timer
带参数装饰器 def decorator_with_args(args): def decorator(func): ... func = decorator_with_args(args)(func) @log(level="DEBUG")

核心要点:

  1. 带参数的装饰器是一个三层的嵌套结构
  2. 最外层函数接收装饰器的参数,并返回中间层的装饰器函数。
  3. 中间层函数接收原始函数,并返回最内层的包装函数。
  4. @functools.wraps 是一个好习惯,它能保留原函数的元信息,便于调试和文档化。
  5. 在包装函数中使用 *args, **kwargs 可以让你的装饰器适用于任何参数的函数。

掌握了带参数的装饰器,你就可以编写出高度可复用、可配置的强大功能模块,

  • 性能分析器:可以传入采样频率。
  • 缓存装饰器:可以设置缓存大小和过期时间。
  • 权限校验:可以传入允许的角色列表。
  • 日志装饰器:可以配置日志级别和输出格式。
-- 展开阅读全文 --
头像
煜升楼宇智能对讲系统
« 上一篇 今天
智能手机充电,怎样才最好?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

最近发表

标签列表

目录[+]