如何组合装饰器?如何让日志输出subprocess.run调用位置的行号?
解决日志打印调用行号的问题
首先,你遇到的问题是因为logging.info默认记录的是它自身被调用的位置(也就是装饰器里的第9行),而不是subprocess.run被调用的位置。要获取真正的调用行号,我们可以借助inspect模块读取调用栈信息,找到上层调用的帧上下文。
实现方法:用inspect获取调用者信息
我们可以在装饰器里通过inspect.stack()拿到调用subprocess.run的具体位置,然后把这些信息传递给日志系统。这里提供两种灵活的方式:直接在日志消息里嵌入行号,或者通过extra参数让日志格式使用自定义字段。
修改后的代码示例
#!/usr/bin/env python3 import logging, sys, functools, subprocess, inspect # 调整日志格式,加入自定义的调用者行号和路径字段 logging.basicConfig( stream=sys.stdout, level=logging.INFO, style='{', format='⬬ {caller_lineno:4d} ⬤ {caller_path} ⬮ {msg}' ) def wrap_function(func): @functools.wraps(func) def wrapper(*args, **kwargs): # 获取调用栈:索引0是当前wrapper的帧,索引1是调用wrapper的帧(即调用subprocess.run的地方) caller_frame = inspect.stack()[1] # 准备额外的日志参数 extra = { 'caller_lineno': caller_frame.lineno, 'caller_path': caller_frame.filename } # 传递extra参数,让日志格式使用这些字段 logging.info((args, kwargs), extra=extra) # 手动清理栈帧,避免内存泄漏 del caller_frame return func(*args, **kwargs) return wrapper subprocess.run = wrap_function(subprocess.run) subprocess.run('ls', timeout=10, shell=True) # 这一行的行号会被打印出来
输出效果
运行后,日志会显示调用subprocess.run的那一行(比如示例中的第22行),而不是装饰器里的行号:
⬬ 22 ⬤ ./PycharmProjects/02.py ⬮ (('ls',), {'timeout': 10, 'shell': True})
关于装饰器组合
装饰器组合就是给一个函数同时应用多个装饰器,核心要注意应用顺序——装饰器是从下往上执行的。举个直观的例子:
import functools def log_before(func): @functools.wraps(func) def wrapper(*args, **kwargs): print("🔍 准备执行函数") return func(*args, **kwargs) return wrapper def log_after(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) print("✅ 函数执行完成") return result return wrapper # 先应用log_after,再应用log_before @log_before @log_after def run_command(cmd): print(f"执行命令:{cmd}") run_command("ls")
执行顺序
调用run_command时,输出顺序是:
🔍 准备执行函数 执行命令:ls ✅ 函数执行完成
这是因为装饰器的应用逻辑等价于run_command = log_before(log_after(run_command)),所以调用时先进入log_before的wrapper,再进入log_after的wrapper,最后执行原函数,返回时顺序相反。
注意事项
- 一定要用
functools.wraps(func),它会保留原函数的元信息(比如函数名、文档字符串),否则装饰后的函数名称会变成wrapper,不利于调试和反射。 - 如果装饰器需要接收参数,需要额外封装一层(比如
def decorator(param): def inner(func): ... return inner),调用时要加括号,比如@decorator("param")。
内容的提问来源于stack exchange,提问作者v217
相关产品推荐
相关产品推荐

