You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何组合装饰器?如何让日志输出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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:08:45