杰瑞科技汇

python decorator 参数

装饰器回顾:无参数

我们快速回顾一下最简单的装饰器,装饰器的本质是一个函数,它接收另一个函数作为参数(被装饰的函数),并返回一个新的函数,这个新函数通常会增强或替换原始函数的功能。

python decorator 参数-图1
(图片来源网络,侵删)

核心概念:A(B) -> C

  • B 是你的原始函数。
  • A 是装饰器函数。
  • C 是返回的新函数,它包含了 B 的功能,并可能增加了额外的逻辑。

简单例子:计时装饰器

import time
# 这是一个装饰器函数
def timer(func):
    # wrapper 是一个内部函数,它会替换原始函数
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)  # 执行原始函数
        end_time = time.time()
        print(f"函数 {func.__name__} 执行耗时: {end_time - start_time:.4f} 秒")
        return result  # 返回原始函数的执行结果
    return wrapper
# 使用 @timer 语法来装饰 my_function
@timer
def my_function(n):
    """一个简单的耗时函数"""
    total = 0
    for i in range(n):
        total += i
    return total
# 调用 my_function,实际上是在调用 wrapper 函数
print(f"计算结果: {my_function(1000000)}")

流程分析:

  1. 当 Python 看到 @timer 时,它会执行 timer(my_function)
  2. my_function 函数对象被传递给 timer 函数。
  3. timer 函数内部定义了 wrapper 函数,然后返回 wrapper 这个函数对象。
  4. my_function 这个名字不再指向原来的函数,而是指向了 timer 返回的 wrapper 函数。
  5. 当你调用 my_function(...) 时,实际上是在调用 wrapper(...)

带参数的装饰器

让我们进入正题:如何给装饰器本身传递参数?

python decorator 参数-图2
(图片来源网络,侵删)

需求场景: 假设我们想创建一个日志装饰器,我们可以指定日志的级别,"INFO""DEBUG"

# 我们希望这样使用:
@log_with_level(level="INFO")
def add(a, b):
    return a + b
@log_with_level(level="DEBUG")
def multiply(a, b):
    return a * b

这里的 level="INFO" 就是传递给装饰器 log_with_level 的参数。

实现思路与难点: 如果你直接尝试,会发现行不通:

# 错误的尝试
def log_with_level(level):
    def timer(func): # 错误!这里的 func 不存在
        def wrapper(*args, **kwargs):
            print(f"[{level}] 正在执行函数 {func.__name__}...")
            result = func(*args, **kwargs)
            print(f"[{level}] 函数 {func.__name__} 执行完毕")
            return result
        return wrapper
    return timer # 返回的是 timer 函数

问题在于 @log_with_level(level="INFO") 这个语法,Python 会立即执行 log_with_level(level="INFO"),这个函数的返回值(在这里是 timer 函数)会应用到被装饰的函数(add)上。

正确的结构是三层嵌套函数:

  1. 最外层函数:接收装饰器的参数(如 level),并返回一个真正的装饰器。
  2. 中间层函数:接收被装饰的函数(如 add),并返回一个包装函数。
  3. 最内层函数:执行真正的功能增强逻辑。

正确实现:三层嵌套函数

def log_with_level(level):
    """第一层:接收装饰器的参数,并返回一个装饰器"""
    print(f"装饰器工厂被调用,参数 level={level}")
    def decorator(func):
        """第二层:接收被装饰的函数,并返回一个包装函数"""
        def wrapper(*args, **kwargs):
            """第三层:执行核心逻辑"""
            print(f"[{level}] 正在执行函数 {func.__name__}...")
            result = func(*args, **kwargs)
            print(f"[{level}] 函数 {func.__name__} 执行完毕")
            return result
        return wrapper
    return decorator
# 使用
@log_with_level(level="INFO")
def add(a, b):
    """加法函数"""
    return a + b
@log_with_level(level="DEBUG")
def multiply(a, b):
    """乘法函数"""
    return a * b
print("-" * 20)
print("调用 add 函数:")
result_add = add(3, 5)
print(f"结果: {result_add}")
print("-" * 20)
print("调用 multiply 函数:")
result_multiply = multiply(4, 6)
print(f"结果: {result_multiply}")

执行流程分析:

  1. Python 解释器从上到下执行代码。
  2. 当它遇到 @log_with_level(level="INFO") 时,会立即调用 log_with_level(level="INFO")
  3. log_with_level 函数执行完毕,返回了 decorator 这个函数。add 函数的定义就变成了 add = decorator
  4. 当定义 multiply 时,同理,multiply = decorator(这里的 decorator 是在 log_with_level(level="DEBUG") 调用后返回的)。
  5. 关键点level 的值("INFO""DEBUG")被“捕获”并保存在闭包中,供 wrapper 函数后续使用。

functools.wraps 的重要性

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

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """这是 wrapper 函数的文档字符串"""
        print("Something is happening before the function is called.")
        func(*args, **kwargs)
        print("Something is happening after the function is called.")
    return wrapper
@my_decorator
def say_hello():
    """这是 say_hello 函数的文档字符串"""
    print("Hello!")
print(say_hello.__name__)  # 输出: wrapper
print(say_hello.__doc__)   # 输出: 这是 wrapper 函数的文档字符串

为了解决这个问题,Python 标准库 functools 提供了一个 wraps 装饰器,你应该把它用在你的包装函数上,它会将被装饰函数的元信息复制到包装函数上。

使用 functools.wraps 的最佳实践

import functools
import time
def timer_with_args(arg1, arg2):
    def decorator(func):
        @functools.wraps(func)  # 关键!
        def wrapper(*args, **kwargs):
            print(f"装饰器接收到的参数: arg1={arg1}, arg2={arg2}")
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            print(f"函数 {func.__name__} 执行耗时: {end_time - start_time:.4f} 秒")
            return result
        return wrapper
    return decorator
@timer_with_args("这是参数1", 100)
def calculate_sum(n):
    """计算从 0 到 n-1 的总和"""
    return sum(range(n))
print(f"函数名: {calculate_sum.__name__}")
print(f"文档字符串: {calculate_sum.__doc__}")
print("-" * 20)
result = calculate_sum(1000000)
print(f"计算结果: {result}")

输出:

函数名: calculate_sum
文档字符串: 计算从 0 到 n-1 的总和
--------------------
装饰器接收到的参数: arg1=这是参数1, arg2=100
函数 calculate_sum 执行耗时: 0.0289 秒
计算结果: 499999500000

可以看到,calculate_sum__name____doc__ 都被正确地保留了。


总结与比喻

为了更好地理解,我们可以用一个比喻:

  • 无参数装饰器:像一个外套,你直接把外套(装饰器)穿在函数(衣服)外面。

    • @timer -> 穿上 timer 外套。
  • 带参数的装饰器:像一个可定制的工具箱,你不能直接把工具箱变成外套,你需要先告诉工具箱你需要什么工具(装饰器参数),然后工具箱会给你一个具体的、可用的工具(真正的装饰器),你再用这个工具来包装你的函数。

    • @log_with_level(level="INFO") -> 打开工具箱,告诉它“我需要一个 INFO 级别的日志工具”,工具箱(log_with_level)递给你一个配置好的日志工具(decorator),你用这个工具来包装你的函数。

核心要点:

  1. 语法糖@decorator 等价于 my_func = decorator(my_func)
  2. 带参数的装饰器是三层的
    • 最外层:接收参数,返回一个装饰器。
    • 中间层:接收函数,返回一个包装器。
    • 最内层:执行逻辑。
  3. 闭包:中间层和最内层函数形成闭包,使得最内层函数可以访问到最外层传入的参数。
  4. 最佳实践:始终在包装器函数上使用 @functools.wraps(func) 来保留原始函数的元信息。

掌握了带参数的装饰器,你就掌握了 Python 中一个非常强大且灵活的工具,可以用来编写高度可配置和可复用的代码。

分享:
扫描分享到社交APP
上一篇
下一篇