1. 装饰器的本质
在 Python 中,函数是对象,可以作为参数传递给另一个函数,也可以作为另一个函数的返回值。
装饰器的本质就是一个高阶函数: 它接收一个函数作为参数,并返回一个新的函数。
2. 从零构建一个装饰器
假设我们有一个核心业务函数 process_data ,现在需要增加 “记录函数执行时间” 的功能,但我们不能直接修改process_data 的内部代码。
因此我们可以定义一个timer函数,它接收目标函数func,在内部定义一个wrapper函数来执行计时逻辑,最后返回这个wrapper。
import time
def timer(func):
# 定义内部包装函数
def wrapper():
start_time = time.time()
func() # 执行原函数
end_time = time.time()
print(f"执行耗时: {end_time - start_time:.4f} 秒")
return wrapper # 返回新的函数对象
# 业务函数(无参数)
def process_data():
time.sleep(1)
print("基础数据处理完成")
# 手动调用装饰器
process_data = timer(process_data)
process_data()
3. 使用语法糖 @
Python 提供了 @ 符号作为语法糖,自动处理上述的赋值过程。将 @timer 放在函数定义之前,等同于执行了 process_data = timer(process_data) 。
@timer
def process_data():
time.sleep(1)
print("数据处理完成")
process_data()
4. 解决参数传递问题
上面的例子有一个缺陷:它只能装饰不需要参数的函数。
随着业务迭代,我们的 process_data 变得更复杂,需要接收参数(例如数据内容和处理延迟)。如果直接使用上面的 timer ,代码会因为 wrapper 不接受参数而报错。
为了让装饰器具有通用性,我们需要在 wrapper 中使用 *args 和 **kwargs 来接收任意参数,并透传给原函数。
def timer(func):
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
def process_data(content, delay=0.5):
time.sleep(delay)
return f"成功处理数据: {content}"
# 调用验证
print(process_data("User_Logs", delay=1.2))
5. 完善元数据 ( functools.wraps )
当你打印被装饰后的 process_data.__name__ 时,你会发现它变成了 wrapper ,而不是 process_data 。这在调试、生成文档或使用某些依赖元数据的框架时会造成困扰。
Python 标准库提供了 functools.wraps 装饰器,专门用于将原函数的元数据(如函数名、文档字符串)复制到 wrapper 函数上。
最终代码:
import time
from functools import wraps
def timer(func):
@wraps(func) # 核心步骤:保留原函数元数据
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
def process_data(content, delay=0.5):
"""模拟核心业务逻辑处理"""
time.sleep(delay)
return f"成功处理数据: {content}"
# 验证元数据
print(f"函数名: {process_data.__name__}") # 输出: process_data
print(f"文档: {process_data.__doc__}") # 输出: 模拟核心业务逻辑处理
总结
关于装饰器,只需掌握以下三个关键点:
定义:装饰器是一种在不改变原函数代码和调用方式的前提下,为其增加新功能的语法糖。
作用:在不修改源码的情况下,实现日志、权限校验、缓存等切面功能。
规范:必须使用 *args, **kwargs 处理参数,并使用 @wraps 保留原函数元数据。