前言
这次又开了个新坑——GitHub探索,主要内容是试水当期GitHub上较火的repo
虽然top榜上各路新手教程跟经典老不死项目占据了大半江山,但清流总是会有的。
第一期就试水一下pysnooper吧,一个新奇实用的python调试器。
顺便源码分析一波,了解下python的debug操作。
废话不多说,进入正题~
pysnooper使用效果
通常,我们可以在函数上用pysnooper.snoop
装饰器,给这个函数包装一个额外功能,实现在标准错误流打印函数debug信息的效果。比如说:
1 |
|
执行main的效果就是:
1 | Starting var:.. x = 2 |
这样我们就可以trace到整一个函数相关的流程了,非常方便,可以很好地代替print的工作。
pysnooper实现原理
pysnooper的实现涉及到python底层debug相关的知识。在以前写过的一篇lua的debug库源码分析中提到了lua获取debug信息的相关操作,而pysnooper实现上也是通过获取底层信息进行debug trace,从结果上来看,也收集了call、line、return以及变量定义之类的操作事件信息。虽然语言不同,但基本思想都一样滴~
因此,在逆向pyssnooper实现原理之时,也将先入为主地代入一些lua的相关概念。
pysnooper相关的参考资料,基本可以在python标准库中的inspect库文档中找到~
调用的pysnooper.snoop
定义在pysnooper的__init__.py
中:
1 | from .tracer import Tracer as snoop |
因此我们直接转向Tracer类一探究竟
1 | def __call__(self, function): |
作为一个装饰器首先要实现的是各类函数的包装。pysnooper首先将该函数编译的代码__code__
进行备份,而后根据情况封装函数。pysnooper暂时没有对协程(async def task/coroutine)做封装,但对于一般函数跟generator,都把函数体内所有的操作包裹在了Tracer自己的with作用域中。
在日常码码中,我们写到with的场景一般是文件io操作,或者是tensorflow之类。with作用域提供了__enter__
与__exit__
两个元方法,定义进出作用域时的相关操作。
1 | def __enter__(self): |
可以看到,每当发生进出Tracer作用域的时候(也就是封装function的时候)都会发生一些类似状态管理的操作。因此首先稍微厘清一些概念:
- frame:相当于lua的callinfo,表示python调用栈上的函数信息
- thread_local:当前线程作用域(Java同学应该都明白)
- trace:相当于lua的hook
可以看到,每次__enter__
时,增加统计frame信息,并且在当前线程建立一个trace栈记录每个函数上一个frame(调用该函数的frame)的trace函数。
然后反过来,每次__exit__
时,trace函数重置为上一个(从trace栈中pop出来),同时移除统计的frame,从而维持原有的状态。
最后我们直接看trace(hook)函数,了解pysnooper打印操作具体实现:
1 | def trace(self, frame, event, arg): |
python规定trace函数包含三个参数:frame、event与arg,frame代表当前调用栈的frame;event是运行时事件,比lua多了exception与opcode两个;arg是受控于event的只读参数。
在pysnooper的trace函数中,首先针对是否记录/打印数据进行判断,只有当前frame或者其上层frame在要测的frames里或者包含要测的代码块,才会被纳入pysnooper记录当中。
判断完之后,就规定每一个函数调用事件发生时,打印增加4位缩进。
1 | def get_local_reprs(frame, watch=()): |
而后,对当前frame的变量状态进行分析。如果有新的变量,则标明新变量或者调用参数;如果有变量跟上一次值不一样,则标明修改了一个变量。
1 | def trace(...): |
然后,除去新建变量/修改变量之外,其它的日志都打印当前时间、event、源码以及线程信息。
对于装饰器,则暂且跳过,寻找真正的函数声明。
1 | def trace(...): |
最后就是return跟exception的判定,两者有一定的交集(可见上面注释),因此根据不同情况从当前作用域变量表locals()
提取不同变量打印不同信息。这里也不需细述。
总体看来,pysnooper提供的hook还是非常轻量实用的。虽然存在这兼容async task跟自定义hook(trace)的问题,但在平时debug中已经可以满足许多需求了。
总结
以前写python的时间算起来应该是最多的,但是python调试相关的工作都没有好好研究过,说来也有点小惭愧。
这次借着试水pysnooper的机会,涨了许多见识,也顺便对python底层有了初步的了解。
虽然自己习惯肉眼debug,但pysnooper作为一个debug黑科技,还是相当给力!!!