Python修饰器如何带参数?

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

不带参数的装饰器

我们快速回顾一下标准装饰器的工作原理,因为带参数的装饰器是在此基础上构建的。

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

一个标准的装饰器本质上是一个函数,它接收一个函数作为参数,并返回一个新的函数。

示例:

def my_decorator(func):
    """一个简单的装饰器,用于在函数执行前后打印信息"""
    def wrapper():
        print("函数执行前...")
        func()  # 调用原始函数
        print("函数执行后...")
    return wrapper
@my_decorator
def say_hello():
    print("Hello, World!")
# 调用被装饰的函数
say_hello()

输出:

函数执行前...
Hello, World!
函数执行后...

过程分解:

python 修饰器 带参数
(图片来源网络,侵删)
  1. 当 Python 解释器看到 @my_decorator 时,它实际上执行了 say_hello = my_decorator(say_hello)
  2. 这意味着 say_hello 这个名字现在指向了 my_decorator 函数返回的 wrapper 内部函数。
  3. 当我们调用 say_hello() 时,实际上是在调用 wrapper()

带参数的装饰器

我们想要让装饰器本身可以接收参数,我们想自定义打印的消息。

问题: 如果我们直接修改 my_decorator,让它接收一个参数 message,会发生什么?

# 错误的尝试
def my_decorator_with_arg(message):
    def wrapper(func):
        def inner_wrapper():
            print(f"函数执行前... ({message})")
            func()
            print(f"函数执行后... ({message})")
        return inner_wrapper
    return wrapper
@my_decorator_with_arg("这是自定义消息")
def say_goodbye():
    print("Goodbye, World!")
say_goodbye()

为什么这样写?

你会发现,带参数的装饰器需要三层嵌套

python 修饰器 带参数
(图片来源网络,侵删)
  1. 最外层函数 (my_decorator_with_arg)

    • 它接收你传递给装饰器的参数"这是自定义消息")。
    • 它的返回值是一个装饰器(也就是 wrapper 函数)。
  2. 中间层函数 (wrapper)

    • 它接收被装饰的原始函数say_goodbye)。
    • 它的返回值是一个包装函数(也就是 inner_wrapper)。
  3. 最内层函数 (inner_wrapper)

    它真正执行原始函数的逻辑,并可以访问到外层函数的参数(通过闭包)。

执行流程分解:

当 Python 解释器看到 @my_decorator_with_arg("这是自定义消息") 时,它会按以下顺序执行:

  1. 首先调用 my_decorator_with_arg("这是自定义消息")
  2. 这个调用会返回 wrapper 函数。
  3. Python 会将 后面的表达式(即返回的 wrapper 函数)应用到 say_goodbye 函数上,这相当于执行 say_goodbye = wrapper(say_goodbye)
  4. say_goodbye 指向的是 inner_wrapper 函数。

上面的代码输出:

函数执行前... (这是自定义消息)
Goodbye, World!
函数执行后... (这是自定义消息)

实战案例:带参数的计时装饰器

让我们来看一个更实用的例子:一个可以指定重复次数的计时装饰器。

import time
import functools
def repeat(times):
    """
    一个带参数的装饰器,用于重复执行函数并计时。
    :param times: 重复执行的次数
    """
    def decorator(func):
        @functools.wraps(func)  # 好习惯,保留原始函数的元信息(如 __name__, __doc__)
        def wrapper(*args, **kwargs):
            total_time = 0
            print(f"--- 开始执行函数 '{func.__name__}' {times} 次 ---")
            for i in range(times):
                start_time = time.perf_counter()
                result = func(*args, **kwargs)
                end_time = time.perf_counter()
                total_time += (end_time - start_time)
                print(f"  第 {i+1} 次执行耗时: {end_time - start_time:.6f} 秒")
            average_time = total_time / times
            print(f"--- 总耗时: {total_time:.6f} 秒, 平均耗时: {average_time:.6f} 秒 ---")
            return result
        return wrapper
    return decorator
# 使用装饰器
@repeat(times=3)
def process_data(data):
    """模拟一个数据处理函数"""
    print(f"正在处理数据: {data}")
    time.sleep(0.5) # 模拟耗时操作
# 调用被装饰的函数
process_data("sample_data.txt")

输出:

--- 开始执行函数 'process_data' 3 次 ---
正在处理数据: sample_data.txt
  第 1 次执行耗时: 0.500123 秒
正在处理数据: sample_data.txt
  第 2 次执行耗时: 0.500456 秒
正在处理数据: sample_data.txt
  第 3 次执行耗时: 0.500789 秒
--- 总耗时: 1.501368 秒, 平均耗时: 0.500456 秒 ---

代码解析:

  • repeat(times) 是最外层函数,接收参数 times
  • decorator(func) 是中间层函数,接收被装饰的函数 process_data
  • wrapper(*args, **kwargs) 是最内层函数,它使用 *args**kwargs 来接收 process_data 的任意参数,使其成为一个通用的装饰器。
  • @functools.wraps(func) 是一个非常好的实践,它将原始函数的 __name__, __doc__ 等属性复制到 wrapper 函数上,这样在调试或使用 help() 时,信息会更准确。

如何记忆和理解

你可以把带参数的装饰器想象成一个“装饰器工厂”

  • 不带参数的装饰器:你直接拿到一个装饰器,把它用在函数上。

    @my_decorator
    def my_func(): ...
    # 等价于 my_func = my_decorator(my_func)
  • 带参数的装饰器:你先调用一个“工厂函数”,这个工厂根据你给的参数,为你量身定做一个合适的装饰器,然后再把这个定制的装饰器用在函数上。

    @my_decorator_factory(arg1, arg2)
    def my_func(): ...
    # 等价于 my_func = my_decorator_factory(arg1, arg2)(my_func)
    # 这分两步:
    # 1. decorator = my_decorator_factory(arg1, arg2)
    # 2. my_func = decorator(my_func)

记住这个三层嵌套的结构,你就能轻松理解并写出任何复杂的带参数装饰器了:

  1. 第一层:接收装饰器参数。
  2. 第二层:接收原始函数。
  3. 第三层:执行核心逻辑,并可以访问到第一层的参数和第二层的函数。
-- 展开阅读全文 --
头像
联想3.5寸智能手机,为何是罕见小屏?
« 上一篇 今天
灵越15燃7560拆机后内部结构如何?
下一篇 » 今天
取消
微信二维码
支付宝二维码

最近发表

标签列表

目录[+]