news 2026/9/21 1:44:05

PySnooper源码剖析②:深入sys.settrace与f_trace机制,如何钩住函数执行的每一行?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PySnooper源码剖析②:深入sys.settrace与f_trace机制,如何钩住函数执行的每一行?

PySnooper源码剖析②:深入sys.settrace与f_trace机制,如何钩住函数执行的每一行?

【免费下载链接】PySnooperNever use print for debugging again项目地址: https://gitcode.com/gh_mirrors/py/PySnooper

PySnooper 是一个轻量级 Python 调试追踪工具,口号是"Never use print for debugging again"。本篇源码剖析将深入它最核心的两个机制——sys.settrace与帧对象的f_trace属性,看懂 PySnooper 是如何"钩住"函数执行的每一行、并顺手记录所有局部变量变化的。读完你可以用不到 50 行代码复刻一个迷你版。

🧭 PySnooper 工作原理全景:装饰器只是入口

先看整体架构,pysnooper/init.py 只做了一件事:把Tracer类以snoop的名字导出。无论你是写:

  • @pysnooper.snoop()装饰一个函数,
  • 还是with pysnooper.snoop():包裹一段代码,

最终都会走进同一个上下文管理器:_wrap_function里生成的包装函数在调用原函数前先进入with self:(见 pysnooper/tracer.py)。真正的"钩子"就藏在这个上下文管理器的__enter__/__exit__里。

下面是 PySnooper 追踪一个函数时的实际输出效果,每行代码执行时都会被逐行"钩住"并打印时间戳与行号:

🔌 钩子一:sys.settrace 全局开关

打开 pysnooper/tracer.py 的__enter__,最后一行就是全局开关:

sys.settrace(self.trace)

这行调用的作用范围是:本线程此后发生的所有函数调用。每当解释器要调用一个新函数,就会以trace(帧对象, 事件名, 参数)的形式回调我们注册的函数,事件名有四种:call(被调用)、line(即将执行某行)、return(即将返回)、exception(抛出异常)。

但它有个关键盲区:sys.settrace钩不住"当前正在执行"的那个函数——调用发生时,这个函数早已开始运行了。这就是为什么还需要第二个钩子。

另外注意__enter__里还做了一件容易忽略的事(pysnooper/tracer.py):先把sys.gettrace()拿到旧处理器,压入一个线程私有的栈。这样支持嵌套使用,退出时才能精确恢复现场。

🎯 钩子二:f_trace 局部钩子——补上最后拼图

__enter__的另一行才是"逐行钩住"的真正钥匙(pysnooper/tracer.py):

calling_frame.f_trace = self.trace

  • calling_frame通过inspect.currentframe().f_back拿到,正是用户代码里执行with语句的那个栈帧(frame)对象
  • 给帧对象的f_trace属性赋一个函数,等于告诉解释器:"这个帧之后的每条指令执行前,先回调我这个函数";
  • 立刻生效,不受sys.settrace的时间点限制。

两个钩子分工明确:

钩子作用范围解决的问题
sys.settrace全局:之后所有新函数调用让被追踪函数内部调用的子函数也能被捕获(配合depth参数下钻)
frame.f_trace局部:单个栈帧钩住"已经在运行"的当前函数,实现逐行追踪

退出上下文时,__exit__sys.settrace(stack.pop())恢复旧处理器,并把帧移出追踪集合(pysnooper/tracer.py),干净利落不留副作用。

📋 trace 回调解剖:一行行日志是怎么生成的

回调函数 trace 每执行一行代码都会被调用一次,频率极高,因此源码里处处是"为高频路径做超优化"的痕迹:

  1. 先做最便宜的过滤:判断frame.f_code in self.target_codes是否成立(pysnooper/tracer.py)。不在追踪目标的帧直接返回None停掉追踪——这是最常见的快速退出路径。若设置了depth>1,则沿f_back链向上回溯,看祖先帧是否是被追踪函数。
  2. 取行号与源码行frame.f_lineno给出当前行号,源码内容通过带缓存的get_path_and_source_from_frame读取(pysnooper/tracer.py)。
  3. 对比局部变量get_local_reprs读取frame.f_locals,并与上一次的值对比,从而输出New var/Modified var(pysnooper/tracer.py)。watch参数支持追踪任意表达式,其求值逻辑在 pysnooper/variables.py 中。
  4. 处理四种事件
事件含义PySnooper 的动作
call函数被调用打印Starting var,调用深度 +1
line即将执行某行打印时间戳 + 行号 + 源码行
return函数返回打印Return value,清理帧状态
exception抛出异常打印异常信息

有两个细节非常巧妙:

  • 如何区分"正常返回"和"异常导致的返回"?异常结束时解释器仍会发return事件且参数为None,call_ended_by_exception 借助frame.f_lasti(最后执行的字节码偏移)反查opcode.opname来判断,从而准确打印Call ended by exception
  • 回调末尾返回self.trace(pysnooper/tracer.py):trace 函数的返回值决定"下一个被调用的函数"由谁继续追踪。返回自己,子函数的调用链就能被持续钩住——这正是depth参数能够层层下钻的底层原理。

🛡️ 值得抄作业的健壮性细节

  • 线程安全:每个线程用threading.local()维护独立的原始 trace 函数栈,多线程下互不干扰,配合thread_info=True还能在输出中标记线程 ID;
  • 一键熔断:设置环境变量PYSNOOPER_DISABLED=1即可让所有追踪原地失效(pysnooper/tracer.py),生产环境误留装饰器也不炸,详见 ADVANCED_USAGE.md;
  • 跳过自身_is_internal_frame通过比对co_filename排除 PySnooper 内部帧,避免"追踪追踪器"的递归(pysnooper/tracer.py);
  • 源码随处可取source_and_path_cache缓存源码,并对 IPython、Jupyter、Ansible zip 包等特殊运行环境逐一兜底(pysnooper/tracer.py);
  • 输出可控:变量值经get_shortish_repr截断,防止一个巨型列表把日志撑爆(pysnooper/utils.py)。

📝 小结:50 行复刻"迷你版 PySnooper"

PySnooper 的核心其实只有三句话:

  1. sys.settrace(fn)钩住将来的所有函数调用;
  2. frame.f_trace = fn钩住当下正在执行的帧;
  3. 在回调里根据event读写f_linenof_locals,把执行过程翻译成人类可读的日志。

理解了这套sys.settrace+f_trace的双钩子机制,你不仅看懂了 PySnooper 的源码,也拿到了 Python 调试器、性能分析器(如 cProfile)背后的同一把钥匙。

【免费下载链接】PySnooperNever use print for debugging again项目地址: https://gitcode.com/gh_mirrors/py/PySnooper

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 1:41:06

高质量数据集建设与标准化:从数据治理到质量评估实战

简介:一份四十页PPT资源,聚焦高质量数据集建设与标准化情况,面向人工智能从业者、数据工程师、大模型训练及数据治理相关读者。内容从数据驱动的人工智能发展切入,回顾浅层学习、深度学习到大模型时期数据集规模与质量要求的演进&…

作者头像 李华
网站建设 2026/9/21 1:38:53

国内下载GR00T N1.7模型:解决Hugging Face无法连接的完整方案

说实话,第一次要在国内环境里把 GR00T N1.7 这套模型下载到本地,我本以为顶多就是敲几条命令的事。结果真到了那一步,先是 huggingface 官网直接连接超时,接着各种下载工具轮番报错,折腾了大半个下午才把模型文件完整拉…

作者头像 李华
网站建设 2026/9/21 1:36:12

压缩二氧化碳储能耦合LNG冷能:Ebsilon系统建模仿真与工程实践

1. 项目概述前段时间在做储能方向的技术调研时,我注意到一个很有意思的系统方案——压缩二氧化碳储能耦合液化天然气冷能。很多人一听储能,第一反应还是抽水蓄能、电化学储能这些主流路线,但压缩气体储能其实是另一个正在快速升温的方向。像二…

作者头像 李华
网站建设 2026/9/21 1:35:56

大模型推理优化:Prefill、Decode与KV Cache的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/21 1:35:53

四款AI Agent深度对比:OpenClaw、Hermes、Claude Code与Codex CLI

1. 这四款 AI Agent 的真实定位:别再把它们混为一谈先说个我观察到的现象:最近半年,几乎每周都有人在社群里问"OpenClaw 和 Claude Code 哪个好用""Hermes Agent 能不能替代 Codex CLI"。表面上看,这几个工具…

作者头像 李华