bigywater.tech
Python · 23

sys.settrace 与调试器:Python 如何逐行跟踪代码

给一个跑3次循环的函数装上sys.settrace,逐个看它触发的call/line/return事件序列,理解pdb单步调试的底层机制和代价。

11个事件
n=3的循环触发的事件总数
4次vs3次
for判断行和循环体行的触发次数差
全局逐行钩子
settrace对之后所有调用生效,不止一个函数

做的事:给一个函数装一个逐行触发的钩子

pdb、coverage.py这类工具,底层都是靠这一个函数实现的。

代码events = [] def tracer(frame, event, arg): if frame.f_code.co_name == "traced_fn": events.append((event, frame.f_lineno)) return tracer def traced_fn(n): total = 0 for i in range(n): total += i return total sys.settrace(tracer) traced_fn(3) sys.settrace(None)

验证:一次调用触发了11个事件

n=3,循环体理应跑3次,但事件数不是"3次"这么简单。

事件序列('call', 35) ('line', 36) ('line', 37) ('line', 38) ('line', 37) ('line', 38) ('line', 37) ('line', 38) ('line', 37) ('line', 39) ('return', 39) 总事件数: 11

拆解:call 事件在函数刚被调用、还没执行第一行时触发一次;之后每执行一条新的源码行,都会触发一次line事件——第37行(for i in range(n))和第38行(total += i)交替出现了4次和3次,因为for循环每次判断"还有下一个i"都要回到第37行,n=3时循环体跑了3趟(第38行出现3次),但第37行的判断跑了4趟(前3趟成功进入循环体,第4趟判断失败跳出循环,直接走到第39行);最后return 事件在函数要返回之前触发一次。

验证:追踪器只会命中被显式检查的那个函数

tracer函数里有一句if frame.f_code.co_name == "traced_fn"的过滤。

这行过滤揭示了sys.settrace的一个关键代价:一旦调用了 sys.settrace(tracer),tracer会被Python解释器对之后调用的每一个函数触发(不止是traced_fn),如果不加这句按函数名过滤,events列表会被print、events.append本身这些调用也一起记录进去,甚至陷入递归膨胀。这正是为什么pdb这类调试器一旦挂上,程序会明显变慢——sys.settrace是一种全局逐行钩子,Python解释器每执行一行源码代码都要检查一次"现在是不是在被追踪",这份检查代价是逐行叠加的,跟被调试代码本身跑了多少行直接成正比。

分步演示:settrace怎么逐行跟踪

每一步都是本文上面用sys.settrace跑出来的原始事件序列。

一个跑3次循环的函数触发了11个追踪事件
这一步新验证的事件

    本文用到的工具

    • 本机 Python 3.14.7
    • sys.settrace:记录一次函数调用的call/line/return事件序列
    京ICP备2026067502号-1京公网安备11010502063487号