Python 中的装饰器传参三层嵌套

FreeGuideOnline 最新 2026-07-07

为什么装饰器需要传参?

装饰器是 Python 中用来修改函数行为的强大工具。大多数初学者最先接触到的是无参装饰器,它们直接“包裹”目标函数。但实际项目中,我们常常希望装饰器本身能接受参数,以便控制装饰逻辑,例如限制重试次数、设置超时时间或指定日志级别。

此时,简单的两层嵌套就不再适用了,必须引入第三层嵌套。这一层用于接收装饰器的配置参数,并返回一个真正的装饰器函数。

理解这种结构的关键在于:@decorator 语法本质上是一个函数调用
当装饰器带参数时,@decorator(args) 首先被求值,它必须返回一个标准的无参装饰器,然后再去包裹目标函数。

从无参装饰器回顾

先看一个最简单的无参装饰器,它统计函数运行时间:

import time

def timer(func):                     # 第一层:接收函数
    def wrapper(*args, **kwargs):    # 第二层:包裹逻辑
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 运行耗时: {end - start:.4f}s")
        return result
    return wrapper

@timer
def sleep_abit():
    time.sleep(0.5)
    print("醒来")

sleep_abit()

输出:

醒来
sleep_abit 运行耗时: 0.5005s

在这个过程中,@timer 等价于 sleep_abit = timer(sleep_abit)。只有两层函数:外层 timer 接收函数,内层 wrapper 是实际调用的新函数。

有参装饰器的三层嵌套结构

如果我们需要让 timer 接受一个参数,比如指定时间单位(秒或毫秒),或者决定是否打印日志,就要改造为三层嵌套。

架构图示

@decorator(param)
   │
   └──> 1. 调用 decorator(param) → 返回真正的装饰器 real_decorator
        2. 然后把目标函数传递给 real_decorator(func)
        3. real_decorator(func) 返回最终的 wrapper 函数

用伪代码表示就是:

def decorator(param):          # 最外层:接收装饰器参数
    def actual_decorator(func): # 中间层:接收函数(这才是真正的装饰器)
        def wrapper(*args, **kwargs): # 最内层:新功能包裹层
            # 可以使用 param 和 func
            result = func(*args, **kwargs)
            return result
        return wrapper
    return actual_decorator    # 返回装饰器

完整实例:带精度控制的计时器

我们实现一个 timer 装饰器,可以接受参数 precision 控制时间打印的小数位数,以及一个布尔值 log 决定是否输出。

import time
from functools import wraps

def timer(precision=4, log=True):
    """最外层:接收装饰器参数"""
    def decorator(func):
        """中间层:真正的装饰器,接收原函数"""
        @wraps(func)
        def wrapper(*args, **kwargs):
            """最内层:包裹函数,实现计时逻辑"""
            start = time.time()
            result = func(*args, **kwargs)
            end = time.time()
            if log:
                time_str = f"{end - start:.{precision}f}"
                print(f"[计时] {func.__name__} 耗时: {time_str}s")
            return result
        return wrapper
    return decorator

# 使用带参数的装饰器
@timer(precision=6, log=True)
def fast_calc():
    return sum(range(100000))

@timer(precision=2, log=True)
def slow_calc():
    time.sleep(1.2)
    return 42

print(fast_calc())
print(slow_calc())

输出示例:

[计时] fast_calc 耗时: 0.002456s
4999950000
[计时] slow_calc 耗时: 1.20s
42

请注意:

  • timer(precision=6, log=True) 得到的是一个装饰器 decorator,然后 decorator(fast_calc) 返回 wrapper
  • 我们使用了 functools.wraps 保持原函数的元信息(如 __name____doc__)。
  • 最内层 wrapper 可以自由访问 precisionlog,它们是闭包变量。

逐层拆解执行流程

对于 @timer(log=False) 修饰的函数:

  1. Python 解释器遇到 @timer(log=False)
    先计算 timer(log=False),调用最外层函数,返回 decorator 函数对象。
  2. 用返回的 decorator 包裹下一行的函数
    这相当于执行 decorator(target_function)decorator 接收函数并返回 wrapper
  3. 将原函数名重新绑定到 wrapper
    当用户调用 target_function(...) 时,实际执行的是 wrapper(...)

图解代码化:

# @timer(log=False) 底层等价于:
def some_func():
    pass

some_func = timer(log=False)(some_func)

timer(log=False) 是第一次调用,返回 decorator;再传入 some_func 得到 wrapper

三层嵌套中的重要细节

1. 保持函数签名正确

使用 functools.wraps(而不是手动复制 __name__)是强烈推荐的做法。它能让 help()、文档生成工具和内省机制正常工作。

from functools import wraps

def my_decorator(arg1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # ...
            return func(*args, **kwargs)
        return wrapper
    return decorator

2. 参数的默认值

三层嵌套让装饰器具有默认参数成为可能,这样调用时可以省略括号:

def repeat(times=2):          # 默认重复2次
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat()          # 使用默认值2,括号不能省略(除非你再做特殊处理)
def greet():
    print("你好")

@repeat(times=3)   # 显式指定3次
def attention():
    print("重要的事情说三遍")

如果希望 @repeat 不带括号也能工作,通常需要把装饰器写成一个能同时处理“无括号”和“有括号”的单一函数,但这样会引入更多复杂性,初学者现阶段应优先掌握清晰的三层形式。

3. 多个装饰器参数

三层嵌套很容易扩展:最外层函数可以接收任意参数和关键字参数。

def log_info(level="INFO", prefix="LOG"):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{prefix}][{level}] 调用 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

常见误区与调试技巧

  • 忘记返回中间层装饰器
    最外层函数必须返回内部装饰器,否则会得到 NoneType 错误。

  • 混淆各层职责
    最外层只负责接收配置参数;中间层才是真正的装饰器,它接收函数;最内层是具体的包裹逻辑。

  • 闭包变量引用问题
    wrapper 中引用的外部变量(如 precision)会被妥善保存在闭包中,但注意不要错误地修改不可变类型后仍期望外层生效。通常只是读取它们,所以很安全。

  • 如何快速验证结构?
    可以在每一层添加 print 观察调用顺序:

    def test_dec(arg):
        print(f"最外层被调用, arg={arg}")
        def dec(func):
            print(f"中间层被调用, func={func.__name__}")
            @wraps(func)
            def wrapper(*args, **kwargs):
                print("最内层包裹函数执行")
                return func(*args, **kwargs)
            return wrapper
        return dec
    

    修饰一个函数时,会立即看到前两层输出,而最内层输出只在函数实际调用时出现。

实战:可配置的重试装饰器

下面是一个完整的重试装饰器,接受重试次数和异常类型作为参数,具备实际应用价值。

import time
from functools import wraps

def retry(max_retries=3, delay=0, exceptions=(Exception,)):
    """
    可传参的重试装饰器
    :param max_retries: 最大重试次数
    :param delay:        每次重试间隔(秒)
    :param exceptions:   需要重试的异常类型元组
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt < max_retries:
                        print(f"重试 {attempt+1}/{max_retries},原因: {e}")
                        time.sleep(delay)
            # 所有重试耗尽
            raise last_exception
        return wrapper
    return decorator

# 使用示例
import random

@retry(max_retries=2, delay=0.5, exceptions=(ValueError,))
def unstable_operation():
    if random.random() < 0.7:
        raise ValueError("随机失败")
    return "成功"

try:
    print(unstable_operation())
except ValueError as e:
    print(f"最终失败: {e}")

这个例子中,retry(max_retries=2, delay=0.5, exceptions=(ValueError,)) 清晰地展示了三层嵌套如何将配置参数牢牢锁定在闭包中,所有重试逻辑完全隔离在装饰器内,业务函数无需关心。

总结

  • 当装饰器自身需要参数时,必须采用三层函数嵌套
  • 最外层负责接收参数并返回真正的装饰器;中间层是标准的无参装饰器,接收原函数;最内层是实现增强功能的包裹函数。
  • @decorator(args) 语法会先计算 decorator(args),其返回值必须是一个装饰器函数。
  • 始终使用 functools.wraps 保留原函数的元数据。
  • 多练习写几个有参装饰器(计时、重试、权限检查、日志),三层结构会变得非常自然。

掌握有参装饰器的三层嵌套,你就彻底理解了 Python 装饰器的内部机制,并为编写更高级的元编程模式打下了坚实基础。