杰瑞科技汇

Python装饰器如何优雅实现日志功能?

目录

  1. 为什么需要装饰器日志? (理解其价值)
  2. 一个简单的日志装饰器 (基础实现)
  3. 让日志装饰器更强大 (增加参数和灵活性)
  4. 使用 functools.wraps (修复元信息丢失问题)
  5. 一个完整的、可配置的日志装饰器 (最佳实践)
  6. 装饰器栈 (多个装饰器如何工作)

为什么需要装饰器日志?

想象一下,你有一个项目,里面有很多函数和方法:

Python装饰器如何优雅实现日志功能?-图1
(图片来源网络,侵删)
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
# 其他函数也需要重复这样的代码...

问题所在:

  • 代码重复:每个函数都要写一遍日志逻辑,非常冗余。
  • 侵入性强:日志代码污染了业务逻辑,使得函数本身不够纯粹。
  • 维护困难:如果需要修改日志格式或添加新功能,你需要修改所有函数。

装饰器的解决方案: 装饰器可以将这些通用的、非核心的功能(如日志、计时、权限检查)从业务逻辑中剥离出来,装饰”到任何你需要的函数上,实现“关注点分离”。

Python装饰器如何优雅实现日志功能?-图2
(图片来源网络,侵删)

一个简单的日志装饰器

我们先来看一个最基础的日志装饰器,它只记录函数的开始和结束。

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 执行完成 ---
发送成功

代码解析:

  1. log_execution(func): 这是装饰器函数,它接收一个函数 func 作为参数(也就是被 @log_execution 修饰的那个函数)。
  2. wrapper(*args, **kwargs)* 这是内部定义的包装函数,`argskwargs` 确保它可以接收任意数量和类型的参数,使其具有通用性。
  3. *`func(args, kwargs)`: 在 wrapper 内部,我们调用了原始的 func 函数,并将所有参数传递给它。
  4. return wrapper: 装饰器最终返回的是 wrapper 这个新函数,当 Python 看到 @log_execution 时,它实际上是执行了 process_data = log_execution(process_data)

让日志装饰器更强大

上面的装饰器很基础,但我们可以让它更智能,记录参数和执行时间。

Python装饰器如何优雅实现日志功能?-图3
(图片来源网络,侵删)
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))

执行顺序是:

  1. 最先调用 decorator_two,它返回一个包装器 wrapper2
  2. decorator_one 接收 wrapper2 作为参数,并返回另一个包装器 wrapper1
  3. 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 语言设计哲学的深刻理解。

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