python(79) 底层代码追踪工具

1.概述

sys.settrace() 是 Python 提供的底层代码追踪工具,能监控程序执行的每一步(如函数调用、行执行、异常触发),常用于调试、性能分析、代码覆盖率统计等场景

sys.settrace(tracefunc) 会为当前线程设置一个追踪函数 tracefunc,Python 解释器在执行代码时,会在以下关键节点调用这个函数:

  • 进入 / 退出函数(包括内置函数)
  • 执行每一行代码
  • 触发异常
  • 执行 return 语句

2. 追踪函数的参数

tracefunc 必须接收 3 个参数,返回值为「新的追踪函数」(或 None 停止追踪):

复制代码
def tracefunc(frame, event, arg):
    """
    frame: 栈帧对象(包含当前执行的代码上下文,如行号、函数名、局部变量)
    event: 触发事件类型(字符串),常见值:
        - 'call':进入函数/方法
        - 'line':执行一行代码
        - 'return':函数返回
        - 'exception':触发异常
        - 'opcode':执行单个字节码(需特殊开启)
    arg: 事件关联的附加参数(如 'return' 时是返回值,'exception' 时是异常元组)
    """
    return tracefunc  # 返回自身,持续追踪;返回None则停止当前栈的追踪

3. pytest中的应用

python 复制代码
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
    """在测试函数执行前/后捕获函数调用(含注释、入参、返回值)"""

    global _test_function_calls
    _test_function_calls = []  # 重置记录

    # 保存PyDev调试器的原始跟踪函数(修复警告)
    original_trace_func = sys.gettrace()

    def trace_func(frame, event, arg):

        #仅监控调用 与返回
        if event not in ('call', 'return'): return trace_func

        TRACK_MODULE_PREFIXES = [
            "test_",  # pytest测试脚本(以test_开头的模块)
            "ActionWords"  # 你的设备操作库(如device.ai/ao所属的模块名,替换为实际值)
        ]

        # 2. 排除的系统/框架模块(可根据实际输出补充)
        EXCLUDE_MODULES = [
            "tempfile", "codecs", "pytest", "allure", "pluggy",
            "_pytest", "traceback", "sys", "os", "io", "logging"
        ]
        # 3. 排除的函数名(框架/底层函数)
        EXCLUDE_FUNC_NAMES = [
            "__getattr__", "func_wrapper", "reset", "__enter__", "__exit__",
            "write", "read", "flush", "close"  # 文件操作相关
        ]

        # 判断是否为test_方法的第一层调用
        caller_frame = frame.f_back
        if not (caller_frame and _is_test_function(caller_frame.f_code)):
            return trace_func

        func_name = frame.f_code.co_name
        # 跳过内置函数/模块级代码(避免干扰)
        if func_name.startswith('<') or frame.f_code.co_filename == '<string>':
            return trace_func

        if event == 'call':
            func = None
            class_name = "无"  # 函数所属类名
            module_path = "未知"  # 函数所属模块路径

            # ========== 1. 优先获取函数对象(普通函数+类方法) ==========
            # 1.1 从模块找普通函数对象
            module = inspect.getmodule(frame)
            if module and hasattr(module, func_name):
                module_obj = getattr(module, func_name)
                if hasattr(module_obj, '__code__') and module_obj.__code__ is frame.f_code:
                    func = module_obj

            # 1.2 从self/cls找类方法对象
            if not func:
                if 'self' in frame.f_locals:
                    self_obj = frame.f_locals['self']
                    func = getattr(self_obj, func_name, None)
                elif 'cls' in frame.f_locals:
                    cls_obj = frame.f_locals['cls']
                    func = getattr(cls_obj, func_name, None)

            # ========== 2. 获取模块路径 ==========
            if func:
                # 方案A:有函数对象 → 取模块完整路径(如:tests.calc.calc_module)
                func_module = inspect.getmodule(func)
                if func_module:
                    module_path = func_module.__name__  # 模块名(推荐)
                    # 可选:获取模块文件的绝对路径
                    # module_path = os.path.abspath(func_module.__file__)
            else:
                # 方案B:无函数对象 → 从栈帧文件名解析模块名
                if frame.f_code.co_filename:
                    # 提取模块名(如:calc.py → calc)
                    module_name = os.path.splitext(os.path.basename(frame.f_code.co_filename))[0]
                    # 若需完整模块路径,需结合项目根目录(示例:假设项目根是当前目录)
                    module_path = f"{module_name}"

            is_business_module = any(module_path.startswith(prefix) for prefix in TRACK_MODULE_PREFIXES)
            if not is_business_module:
                return trace_func

            # ========== 3. 获取所属类名 ==========
            if func:
                # 3.1 实例方法(绑定到实例)
                if inspect.ismethod(func) and hasattr(func, '__self__'):
                    # 实例方法:__self__ 是实例对象 → 取实例的类
                    if not inspect.isclass(func.__self__):
                        class_name = func.__self__.__class__.__name__
                    # 类方法:__self__ 是类对象
                    else:
                        class_name = func.__self__.__name__
                # 3.2 静态方法(无绑定对象)
                elif inspect.isfunction(func):
                    class_name = "无"

            # ========== 4. 提取形参/注释 ==========
            # 形参提取
            param_names = []
            param_values = []
            if func:
                try:
                    sig = inspect.signature(func)
                    param_names = [p for p in sig.parameters if p not in ('self', 'cls')]
                    param_values = [frame.f_locals[p] for p in param_names if p in frame.f_locals]
                except:
                    param_names = list(frame.f_code.co_varnames[:frame.f_code.co_argcount])
                    param_names = [p for p in param_names if p not in ('self', 'cls')]
            if not param_names:
                param_names = list(frame.f_code.co_varnames[:frame.f_code.co_argcount])
                param_names = [p for p in param_names if p not in ('self', 'cls')]
            param_values = [frame.f_locals[p] for p in param_names if p in frame.f_locals]

            # 注释提取
            doc = inspect.getdoc(func).strip() if (func and inspect.getdoc(func)) else "无注释"
            call_obj_msg = get_call_obj(frame, func)

            # ========== 5. 格式化打印(含类名+模块路径) ==========
            all_msg = {'module_path': module_path, 'class_name': class_name, 'func_name': func_name,
                        'doc': doc, 'call_obj': call_obj_msg['call_obj'], 'call_obj_type': call_obj_msg['call_obj_type'],
                        'param_names': param_names if param_names else '无形参',
                        'param_values': param_values if param_values else '无实参'}

            all_msg['param_values'] = param_rebuild(all_msg['param_values'], all_msg['call_obj'], all_msg['func_name'])
            #print('func_msg=======================',all_msg)
            _test_function_calls.append(all_msg)

        elif event == 'return':

            if _test_function_calls and _test_function_calls[-1]['func_name'] == func_name:
                _test_function_calls[-1]['return'] = arg

        return trace_func

    try:
        # 启用跟踪器(先保存原始,再设置自定义)
        sys.settrace(trace_func)
        # 执行测试用例
        yield
    finally:
        # 恢复原始跟踪函数(修复PyDev警告)
        sys.settrace(original_trace_func)


    html_doc = get_org_description(item)
    new_html_content = build_test_step_html(_test_function_calls)
    allure.dynamic.description_html(html_doc + '\n' + new_html_content)

4. 性能影响

settrace() 会显著降低程序运行速度(因为每一行 / 每一步都要调用追踪函数),仅用于调试 / 分析,不可在生产环境默认开启

5. 栈帧对象(frame)常用属性

表格

属性 说明
frame.f_code.co_name 当前执行的函数名
frame.f_lineno 当前行号
frame.f_locals 函数局部变量字典
frame.f_globals 全局变量字典
frame.f_code.co_filename 当前执行的文件名
相关推荐
kgduu3 小时前
js之错误处理
开发语言·前端·javascript
Bert.Cai3 小时前
Python函数的定义与调用
开发语言·python
2501_911088233 小时前
Web开发与API
jvm·数据库·python
带娃的IT创业者3 小时前
Python 异步编程完全指南(五):避坑指南与生态推荐
python·asyncio·aiohttp·异步编程·技术博客·阻塞应对
2501_911088233 小时前
使用Python自动收发邮件
jvm·数据库·python
美式请加冰3 小时前
模拟的介绍和使用
java·开发语言·算法
无限进步_3 小时前
深入解析vector:一个完整的C++动态数组实现
c语言·开发语言·c++·windows·git·github·visual studio
万能的小裴同学3 小时前
C++ 简易的FBX查看工具
开发语言·c++·算法
菜菜小狗的学习笔记3 小时前
剑指Offer算法题(二)栈、队列、堆
java·开发语言