问题历史
装饰器是Python中最强大的工具之一,它允许修改函数或方法的行为。有时并不仅仅需要“封装”一个函数,而是需要通过参数(论证)来配置装饰器。这些情况常见于日志记录、时间检查、访问限制等。
问题
普通装饰器仅接受一个需要封装的函数。当需要将参数传递给装饰器时,语法变得复杂,这常常会导致错误,特别是在函数嵌套和传递 *args/**kwargs 时。
解决方案
带参数的装饰器通过更高阶的函数实现:首先调用外部“装饰”函数并传入参数,然后该函数创建并返回实际的装饰器,而装饰器则接收函数并返回封装:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # 输出: Hello, Python! (3次)
关键特点:
为什么不能像普通装饰器那样实现带参数的装饰器?
如果使用 @decorator,Python会将函数作为参数传递给装饰器。如果添加了括号 (@decorator()),Python会首先调用该函数,然后其结果被解释为装饰器。
def deco(func): # 普通装饰器: @deco def deco_with_args(arg): # 带参数的装饰器: @deco_with_args(arg)
带参数的装饰器与无参数的装饰器在调用级别上有何本质区别?
无参数装饰器接受一个函数作为输入,而带参数的装饰器接受的不是函数,而是参数,并返回装饰器。
如何正确使用functools.wraps,为什么要这样做?
在封装内部使用 functools.wraps(func) 可以保存原始函数的名称、文档字符串及其他元数据,否则这些信息会被wrapper替换,影响调试和内省。
import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator
实现了不考虑参数的装饰器或嵌套函数数量不正确:
def log(level): def wrapper(func): # 错误 — wrapper 应该更深 print(f"Log: {level}") func() # 函数没有被作为装饰器返回 return wrapper @log("INFO") def action(): print("Work!")
优点:
缺点:
使用functools.wraps和正确嵌套函数:
import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duration: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()
优点:
缺点: