目录
- 为什么需要装饰器日志? (理解其价值)
- 一个简单的日志装饰器 (基础实现)
- 让日志装饰器更强大 (增加参数和灵活性)
- 使用
functools.wraps(修复元信息丢失问题) - 一个完整的、可配置的日志装饰器 (最佳实践)
- 装饰器栈 (多个装饰器如何工作)
为什么需要装饰器日志?
想象一下,你有一个项目,里面有很多函数和方法:

def process_data(data):
# ... 一些复杂的数据处理逻辑
return processed_data
def send_notification(user_id, message):
# ... 发送通知的逻辑
pass
def calculate_order_total(items):
# ... 计算订单总额的逻辑
return total
你想要记录这些函数的执行情况,
- 函数何时被调用?
- 传入了什么参数?
- 函数执行了多长时间?
- 是否成功执行,还是抛出了异常?
没有装饰器的方式: 你可能会在每个函数的开头和结尾都加上日志代码。
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def process_data(data):
start_time = time.time()
logging.info(f"函数 process_data 被调用,参数: {data}")
# ... 一些复杂的数据处理逻辑
result = "processed_" + data
end_time = time.time()
logging.info(f"函数 process_data 执行完成,耗时: {end_time - start_time:.4f}秒")
return result
# 其他函数也需要重复这样的代码...
问题所在:
- 代码重复:每个函数都要写一遍日志逻辑,非常冗余。
- 侵入性强:日志代码污染了业务逻辑,使得函数本身不够纯粹。
- 维护困难:如果需要修改日志格式或添加新功能,你需要修改所有函数。
装饰器的解决方案: 装饰器可以将这些通用的、非核心的功能(如日志、计时、权限检查)从业务逻辑中剥离出来,装饰”到任何你需要的函数上,实现“关注点分离”。

一个简单的日志装饰器
我们先来看一个最基础的日志装饰器,它只记录函数的开始和结束。
import time
import logging
# 配置一个简单的日志格式
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
def log_execution(func):
"""这是一个装饰器,用于记录函数的执行情况"""
def wrapper(*args, **kwargs):
# 1. 记录函数调用
logging.info(f"--- 开始执行函数: {func.__name__} ---")
# 2. 执行原始函数
result = func(*args, **kwargs)
# 3. 记录函数执行完成
logging.info(f"--- 函数 {func.__name__} 执行完成 ---")
return result
return wrapper
# --- 使用装饰器 ---
@log_execution
def process_data(data):
"""处理数据的函数"""
time.sleep(1) # 模拟耗时操作
return f"处理后的数据: {data}"
@log_execution
def send_notification(user_id, message):
"""发送通知的函数"""
time.sleep(0.5)
print(f"通知已发送给用户 {user_id}: {message}")
return "发送成功"
# --- 调用函数 ---
print(process_data("原始数据A"))
print(send_notification(123, "您的订单已发货"))
输出结果:
--- 开始执行函数: process_data ---
--- 函数 process_data 执行完成 ---
处理后的数据: 原始数据A
--- 开始执行函数: send_notification ---
通知已发送给用户 123: 您的订单已发货
--- 函数 send_notification 执行完成 ---
发送成功
代码解析:
log_execution(func): 这是装饰器函数,它接收一个函数func作为参数(也就是被@log_execution修饰的那个函数)。wrapper(*args, **kwargs)* 这是内部定义的包装函数,`args和kwargs` 确保它可以接收任意数量和类型的参数,使其具有通用性。- *`func(args, kwargs)`: 在
wrapper内部,我们调用了原始的func函数,并将所有参数传递给它。 return wrapper: 装饰器最终返回的是wrapper这个新函数,当 Python 看到@log_execution时,它实际上是执行了process_data = log_execution(process_data)。
让日志装饰器更强大
上面的装饰器很基础,但我们可以让它更智能,记录参数和执行时间。

import time
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def log_execution_with_details(func):
def wrapper(*args, **kwargs):
# 记录函数名和参数
logging.info(f"调用函数: {func.__name__}, 参数: args={args}, kwargs={kwargs}")
start_time = time.time()
try:
result = func(*args, **kwargs)
# 记录执行成功和耗时
end_time = time.time()
logging.info(f"函数 {func.__name__} 执行成功,耗时: {end_time - start_time:.4f}秒")
return result
except Exception as e:
# 记录执行失败和异常信息
logging.error(f"函数 {func.__name__} 执行失败! 错误: {e}", exc_info=True)
raise # 重新抛出异常,让调用者也能捕获
return wrapper
# --- 使用增强版装饰器 ---
@log_execution_with_details
def divide(a, b):
return a / b
@log_execution_with_details
def another_function():
time.sleep(0.2)
return "Done"
# --- 调用 ---
print(another_function())
print(divide(10, 2))
print(divide(10, 0)) # 这个会抛出异常
输出结果:
INFO:root:调用函数: another_function, 参数: args=(), kwargs={}
INFO:root:函数 another_function 执行成功,耗时: 0.2003秒
Done
INFO:root:调用函数: divide, 参数: args=(10, 2), kwargs={}
INFO:root:函数 divide 执行成功,耗时: 0.0000秒
5.0
INFO:root:调用函数: divide, 参数: args=(10, 0), kwargs={}
ERROR:root:函数 divide 执行失败! 错误: division by zero
Traceback (most recent call last):
File "<stdin>", line 10, in wrapper
File "<stdin>", line 5, in divide
ZeroDivisionError: division by zero
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
改进点:
- 记录参数:
logging.info(f"调用函数: {func.__name__}, 参数: args={args}, kwargs={kwargs}") - 记录执行时间:通过
time.time()计算。 - 异常处理:使用
try...except捕获并记录异常,同时使用exc_info=True可以打印完整的堆栈跟踪信息,非常有助于调试。raise关键字确保异常不会在装饰器中被“吞掉”。
使用 functools.wraps
你可能会注意到一个问题:使用了装饰器后,函数的“元信息”(比如函数名 __name__、文档字符串 __doc__)会丢失。
@log_execution
def example():
"""这是一个示例函数"""
pass
print(example.__name__) # 输出: wrapper
print(example.__doc__) # 输出: None
这会给调试、文档生成等带来麻烦,解决方案是使用 functools.wraps。
functools.wraps 本身也是一个装饰器,它的作用是将被装饰函数的元信息复制到包装器函数上。
import functools
def log_execution(func):
@functools.wraps(func) # 在 wrapper 上应用 wraps
def wrapper(*args, **kwargs):
logging.info(f"--- 开始执行函数: {func.__name__} ---")
result = func(*args, **kwargs)
logging.info(f"--- 函数 {func.__name__} 执行完成 ---")
return result
return wrapper
@log_execution
def example():
"""这是一个示例函数"""
pass
print(example.__name__) # 输出: example
print(example.__doc__) # 输出: 这是一个示例函数
最佳实践:永远在装饰器内部使用 @functools.wraps(func),这是一个约定俗成的规则,能避免很多潜在问题。
一个完整的、可配置的日志装饰器
在实际项目中,我们可能需要更灵活的控制,
- 控制日志级别 (INFO, DEBUG, ERROR)。
- 控制是否记录参数。
- 控制是否记录执行时间。
我们可以通过让装饰器接收参数来实现。
import time
import logging
import functools
# 配置日志
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
def logger_config(level=logging.INFO, log_args=True, log_time=True):
"""
一个可配置的日志装饰器工厂。
返回一个真正的装饰器。
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 记录调用信息
if log_args:
logging.log(level, f"调用函数: {func.__name__}, 参数: args={args}, kwargs={kwargs}")
else:
logging.log(level, f"调用函数: {func.__name__}")
# 记录执行时间
if log_time:
start_time = time.time()
try:
result = func(*args, **kwargs)
# 记录成功信息
if log_time:
end_time = time.time()
logging.log(level, f"函数 {func.__name__} 执行成功,耗时: {end_time - start_time:.4f}秒")
else:
logging.log(level, f"函数 {func.__name__} 执行成功")
return result
except Exception as e:
logging.error(f"函数 {func.__name__} 执行失败! 错误: {e}", exc_info=True)
raise
return wrapper
return decorator
# --- 使用可配置的装饰器 ---
# 场景1: 默认配置,记录所有信息
@logger_config()
def task_one():
"""任务一,比较耗时"""
time.sleep(1)
return "任务一完成"
# 场景2: 只记录函数名和执行结果,不记录参数和时间
@logger_config(level=logging.DEBUG, log_args=False, log_time=False)
def task_two(x):
"""任务二,很快"""
return x * 2
# 场景3: 只记录错误,不记录成功信息 (通过设置 log_time=False 来避免记录成功耗时)
@logger_config(level=logging.ERROR, log_args=True, log_time=False)
def risky_operation(a, b):
"""一个有风险的操作"""
if b == 0:
raise ValueError("除数不能为零")
return a / b
# --- 调用 ---
print(task_one())
print(task_two(5))
# risky_operation(10, 0) # 会触发错误日志
输出结果:
DEBUG:root:调用函数: task_one, 参数: args=(), kwargs={}
DEBUG:root:函数 task_one 执行成功,耗时: 1.0005秒
任务一完成
DEBUG:root:调用函数: task_two
DEBUG:root:函数 task_two 执行成功
10
ERROR:root:调用函数: risky_operation, 参数: args=(10, 0), kwargs={}
ERROR:root:函数 risky_operation 执行失败! 错误: 除数不能为零
Traceback (most recent call last):
...
ValueError: 除数不能为零
代码解析:
logger_config(...)是一个装饰器工厂,它接收配置参数,并返回一个真正的装饰器decorator。- 这种多层嵌套的结构是带参数装饰器的标准写法,提供了极大的灵活性。
装饰器栈
一个函数可以被多个装饰器修饰,它们会按照从下到上的顺序依次执行。
@decorator_one
@decorator_two
def my_function():
print("原始函数执行")
# 等价于
my_function = decorator_one(decorator_two(my_function))
执行顺序是:
- 最先调用
decorator_two,它返回一个包装器wrapper2。 decorator_one接收wrapper2作为参数,并返回另一个包装器wrapper1。my_function指向的是wrapper1。
调用 my_function() 的实际执行流程是 wrapper1 -> wrapper2 -> 原始函数。
def decorator_one(func):
print("装饰器 one 被调用")
def wrapper_one(*args, **kwargs):
print("执行 one 的包装器前")
result = func(*args, **kwargs)
print("执行 one 的包装器后")
return result
return wrapper_one
def decorator_two(func):
print("装饰器 two 被调用")
def wrapper_two(*args, **kwargs):
print("执行 two 的包装器前")
result = func(*args, **kwargs)
print("执行 two 的包装器后")
return result
return wrapper_two
@decorator_one
@decorator_two
def my_function():
print(" -> 原始函数 my_function 执行")
print("\n开始调用 my_function():")
my_function()
输出结果:
装饰器 two 被调用
装饰器 one 被调用
开始调用 my_function():
执行 one 的包装器前
执行 two 的包装器前
-> 原始函数 my_function 执行
执行 two 的包装器后
执行 one 的包装器后
| 特性 | 描述 | 优点 |
|---|---|---|
| 核心思想 | 将非核心功能(日志、计时、认证)与业务逻辑分离。 | 代码整洁、可维护、可复用。 |
| 基本结构 | def decorator(func): def wrapper(*args, **kwargs): ... func(*args, **kwargs) ... return wrapper |
通用,能处理任意参数的函数。 |
| 增强功能 | 添加 try/except 捕获异常,time.time() 计时。 |
健壮性更强,信息更丰富。 |
functools.wraps |
@functools.wraps(func),用于保留原函数的元信息。 |
调试友好,符合 Pythonic 风格。 |
| 可配置装饰器 | 通过装饰器工厂 def config(...): def decorator(func): ... 实现。 |
灵活性高,适应不同场景需求。 |
| 装饰器栈 | 多个装饰器从下到上依次应用。 | 可以组合多种功能,如 @log_execution @check_permission。 |
掌握装饰器日志是迈向高级 Python 开发者的重要一步,它不仅能让你的代码更优雅,更能体现你对 Python 语言设计哲学的深刻理解。
