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 |
当前执行的文件名 |