PySnooper源码剖析②:深入sys.settrace与f_trace机制,如何钩住函数执行的每一行?
PySnooper源码剖析②深入sys.settrace与f_trace机制如何钩住函数执行的每一行【免费下载链接】PySnooperNever use print for debugging again项目地址: https://gitcode.com/gh_mirrors/py/PySnooperPySnooper 是一个轻量级 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.pycalling_frame.f_trace self.tracecalling_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 每执行一行代码都会被调用一次频率极高因此源码里处处是为高频路径做超优化的痕迹先做最便宜的过滤判断frame.f_code in self.target_codes是否成立pysnooper/tracer.py。不在追踪目标的帧直接返回None停掉追踪——这是最常见的快速退出路径。若设置了depth1则沿f_back链向上回溯看祖先帧是否是被追踪函数。取行号与源码行frame.f_lineno给出当前行号源码内容通过带缓存的get_path_and_source_from_frame读取pysnooper/tracer.py。对比局部变量get_local_reprs读取frame.f_locals并与上一次的值对比从而输出New var/Modified varpysnooper/tracer.py。watch参数支持追踪任意表达式其求值逻辑在 pysnooper/variables.py 中。处理四种事件事件含义PySnooper 的动作call函数被调用打印Starting var调用深度 1line即将执行某行打印时间戳 行号 源码行return函数返回打印Return value清理帧状态exception抛出异常打印异常信息有两个细节非常巧妙如何区分正常返回和异常导致的返回异常结束时解释器仍会发return事件且参数为Nonecall_ended_by_exception 借助frame.f_lasti最后执行的字节码偏移反查opcode.opname来判断从而准确打印Call ended by exception。回调末尾返回self.tracepysnooper/tracer.pytrace 函数的返回值决定下一个被调用的函数由谁继续追踪。返回自己子函数的调用链就能被持续钩住——这正是depth参数能够层层下钻的底层原理。️ 值得抄作业的健壮性细节线程安全每个线程用threading.local()维护独立的原始 trace 函数栈多线程下互不干扰配合thread_infoTrue还能在输出中标记线程 ID一键熔断设置环境变量PYSNOOPER_DISABLED1即可让所有追踪原地失效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 行复刻迷你版 PySnooperPySnooper 的核心其实只有三句话sys.settrace(fn)钩住将来的所有函数调用frame.f_trace fn钩住当下正在执行的帧在回调里根据event读写f_lineno、f_locals把执行过程翻译成人类可读的日志。理解了这套sys.settracef_trace的双钩子机制你不仅看懂了 PySnooper 的源码也拿到了 Python 调试器、性能分析器如 cProfile背后的同一把钥匙。【免费下载链接】PySnooperNever use print for debugging again项目地址: https://gitcode.com/gh_mirrors/py/PySnooper创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考