1. 装饰器基础回顾:从闭包到函数包装
在Python中,装饰器本质上是一个高阶函数,它接受一个函数作为输入并返回一个新的函数。这种设计模式之所以能实现,核心在于Python对函数的"一等公民"特性支持——函数可以像普通变量一样被传递和返回。
闭包(Closure)是理解装饰器的关键前置知识。闭包是指在一个内部函数中访问其外部函数作用域中的变量,即使外部函数已经执行完毕。这种机制使得函数可以"记住"它被创建时的环境。来看一个典型闭包示例:
python复制def outer_func(msg):
def inner_func():
print(msg) # 访问外部函数变量
return inner_func
my_func = outer_func("Hello")
my_func() # 输出: Hello
装饰器就是闭包的一种高级应用。最基本的装饰器形式如下:
python复制def simple_decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
这个简单装饰器会在被装饰函数执行前后打印日志。@语法糖实际上等价于say_hello = simple_decorator(say_hello)。
关键理解:装饰器在导入时(import time)就执行,而不是在函数调用时。这意味着装饰器代码只运行一次,返回的wrapper函数会替代原函数。
2. 带参数装饰器的实现原理
当我们需要装饰器本身也能接受参数时,情况就变得复杂一些。带参数装饰器实际上需要三层嵌套函数:
- 最外层接收装饰器参数
- 中间层接收被装饰函数
- 最内层实现装饰逻辑
这种结构被称为"装饰器工厂"。典型实现如下:
python复制def repeat(num_times): # 装饰器参数
def decorator(func): # 被装饰函数
def wrapper(*args, **kwargs): # 包装逻辑
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet("Alice")
这个@repeat(3)装饰器会让被装饰函数执行指定次数。理解其执行顺序很重要:
repeat(3)首先被调用,返回decorator函数decorator(greet)接着被调用,返回wrapper函数- 最终
wrapper替代了原greet函数
实际开发中常见误区:忘记在wrapper中返回原函数的结果,或者错误处理了
*args, **kwargs参数传递。
3. 装饰器参数的进阶应用场景
3.1 动态配置装饰行为
带参数装饰器的一个强大之处在于可以根据参数动态调整装饰行为。例如,实现一个可配置的缓存装饰器:
python复制import time
from functools import wraps
def cached(ttl=60): # 默认缓存60秒
def decorator(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
timestamp, result = cache[args]
if time.time() - timestamp < ttl:
return result
result = func(*args)
cache[args] = (time.time(), result)
return result
return wrapper
return decorator
@cached(ttl=300) # 缓存5分钟
def expensive_computation(x):
time.sleep(2) # 模拟耗时计算
return x * x
这个例子展示了如何通过装饰器参数控制缓存过期时间,比固定行为的装饰器灵活得多。
3.2 多参数装饰器的实现
装饰器也可以接受多个参数,只需在外层函数中定义即可。例如实现一个重试机制装饰器:
python复制def retry(max_retries=3, delay=1, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
retries = 0
while retries < max_retries:
try:
return func(*args, **kwargs)
except exceptions as e:
retries += 1
if retries == max_retries:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_retries=5, delay=2, exceptions=(TimeoutError,))
def fetch_data(url):
# 网络请求逻辑
pass
这种装饰器在分布式系统和网络编程中特别有用,可以优雅地处理临时性故障。
4. 装饰器参数与函数签名的交互
4.1 保留原函数元信息
使用装饰器时一个常见问题是会"掩盖"原函数的元信息(如__name__、__doc__等)。Python的functools.wraps装饰器可以解决这个问题:
python复制from functools import wraps
def logged(log_level="INFO"):
def decorator(func):
@wraps(func) # 保留原函数元信息
def wrapper(*args, **kwargs):
print(f"[{log_level}] Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@logged(log_level="DEBUG")
def calculate(x, y):
"""Perform important calculation"""
return x + y
print(calculate.__name__) # 输出: calculate
print(calculate.__doc__) # 输出: Perform important calculation
4.2 类型提示与装饰器参数
在现代Python代码中,类型提示越来越重要。带参数装饰器也可以很好地支持类型提示:
python复制from typing import Callable, TypeVar, Any
T = TypeVar('T')
def validate_input(validator: Callable[[Any], bool]):
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
for arg in args:
if not validator(arg):
raise ValueError("Invalid input")
for arg in kwargs.values():
if not validator(arg):
raise ValueError("Invalid input")
return func(*args, **kwargs)
return wrapper
return decorator
def is_positive(x: Any) -> bool:
return isinstance(x, (int, float)) and x > 0
@validate_input(validator=is_positive)
def sqrt(x: float) -> float:
return x ** 0.5
这种模式在构建健壮的API时特别有用,可以确保输入数据符合预期。
5. 装饰器参数的调试与测试技巧
5.1 调试带参数装饰器
调试装饰器代码可能会有些棘手,因为错误堆栈会显示wrapper函数而不是原函数。几个实用技巧:
- 使用
@wraps保留原函数信息 - 在wrapper内部添加详细日志
- 使用
inspect模块获取调用信息:
python复制import inspect
def debug_decorator(verbose=False):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if verbose:
frame = inspect.currentframe()
print(f"Calling {func.__name__} from {frame.f_back.f_code.co_filename}")
return func(*args, **kwargs)
return wrapper
return decorator
5.2 单元测试策略
测试带参数装饰器需要分别测试:
- 装饰器本身的行为
- 被装饰函数的行为
- 两者的交互
使用unittest的示例:
python复制import unittest
from unittest.mock import patch
class TestRetryDecorator(unittest.TestCase):
def test_retry_success(self):
@retry(max_retries=3)
def always_succeed():
return 42
self.assertEqual(always_succeed(), 42)
def test_retry_failure(self):
call_count = 0
@retry(max_retries=3)
def always_fail():
nonlocal call_count
call_count += 1
raise ValueError("Failed")
with self.assertRaises(ValueError):
always_fail()
self.assertEqual(call_count, 3)
对于更复杂的场景,可以使用pytest的参数化测试功能。
6. 实际项目中的最佳实践
6.1 性能考量
装饰器会引入额外的函数调用开销。对于性能敏感的代码,可以考虑:
- 尽量减少装饰器嵌套层数
- 将装饰器逻辑尽可能简化
- 对于计算密集型函数,可以在装饰器中添加缓存
python复制def profile_decorator(output_file=None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
if output_file:
with open(output_file, 'a') as f:
f.write(f"{func.__name__} took {elapsed:.6f}s\n")
else:
print(f"{func.__name__} took {elapsed:.6f}s")
return result
return wrapper
return decorator
6.2 组合多个带参数装饰器
当需要应用多个带参数装饰器时,顺序很重要——最上面的装饰器最先应用,但最后执行:
python复制@decorator1(arg1=1)
@decorator2(arg2=2)
def my_function():
pass
# 等价于
my_function = decorator1(arg1=1)(decorator2(arg2=2)(my_function))
常见模式是将技术性装饰器(如日志、缓存)放在最上面,业务逻辑装饰器放在下面。
6.3 类作为装饰器参数
装饰器参数不仅可以是简单类型,也可以是复杂对象。例如使用类作为参数:
python复制class Validator:
def __init__(self, min_val, max_val):
self.min_val = min_val
self.max_val = max_val
def __call__(self, value):
return self.min_val <= value <= self.max_val
def validate_with(validator):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if not validator(arg):
raise ValueError(f"Value {arg} not in range")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_with(Validator(0, 100))
def set_percentage(p):
print(f"Setting to {p}%")
这种模式在构建复杂验证逻辑时特别有用。
