前言
最近录了一个关于 overlapper 插件的讲解视频。 使用 VSCode 调试 Python 代码的时候翻车了,而且使用外部软件调试会冻结 Python 导致 Maya 没有相应,交互非常不友好。 于是我就有了开发一个 Debug 插件的想法。 主要是要解决断点状态下Maya的GUI响应问题,这样可以很直观看到代码对 Maya 进行了哪些操作。
最开始我是想要在 ptvsd 的模块上调用 API 开发 Debug 工具。 毕竟我自己做的 VScode 扩展就是基于这个原理,可是后来查了一番之后,发现 ptvsd 是基于 PyDev 的 API调用上我挖掘不太深,就是感觉太复杂了,很多杂七杂八的功能都不需要。
而且我想要更加通用性的解决方案,利用代码来插入断点,进入断点之后开启一个 while 死循环来更新 UI 界面。 断点调试的时候就 break 掉死循环就好了。
再进一步研究之后,我发现 Python 自带的 pdb 模块就是实现这个功能的。 于是开始研究 pdb 模块有了今天这篇文章。
PDB 模块使用
PDB 全称是 Python Dubugger,是 python 的内置模块,可以实现命令行来对 python 代码 Debug 具体用法可以参考后端大佬的视频
特点就是在代码里面输入 pdb 的代码,程序会捕获到插入的 pdb 调试断点然后进入断点调试模式。 缺点就是纯命令行工具,所有的信息显示都需要通过键盘输入来进行调试,真 DOS 时代无鼠标的操作。
其实 Maya 的官方博客也有提到过 关于 Maya Python Debug 的方案,其中还说到了 利用 pdb 模块debug 的方法 around-corner地址 不过官方并不推荐使用 pdb ,因为 Maya 的脚本编辑器无法进行命令行输入输出,因此input命令将会弹出一个输入窗口 这样的交互方式是在是太过不友好了,连官方都吐槽了。。。
另外 maya 的官方文档也有提到 Python Debug 的方案 Maya 文档 如何使用 pdb 可以参照 python 的 官方文档 不过官方文档其实也没有什么代码,都是命令解释,所以也不太友好。
VIDEO
youtube上也有视频专门讲解在 Maya 如何使用 pdb debug
pdb 源码探讨 set_trace 源码挖掘
在 pdb 调试中,大多数时候就是在要设置断点的行数前插入一行 pdb 的代码 pdb.set_trace()
那么这一句代码在背后做了什么呢? 我们可以去到 Python 的Lib目录下找到 pdb.py 文件 Maya 的 Python 内置代码封装到了 bin 目录的 python27.zip 压缩包当中 github上也可以查找到源码(代码是Python3的) https://github.com/python/cpython/blob/master/Lib/pdb.py
在代码当中搜索 set_trace 可以找到下面这个函数
1 2 def set_trace (): Pdb().set_trace(sys._getframe().f_back)
Pdb 类里面是找不到 set_trace 函数的 而通过集成关系可以找到Pdb类是继承于 bdb模块的 Bdb 基类 因此我们可以去到 bdb.py 查找 set_trace 的代码 | https://github.com/python/cpython/blob/master/Lib/bdb.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def set_trace (self, frame=None ): """Start debugging from `frame`. If frame is not specified, debugging starts from caller's frame. """ if frame is None : frame = sys._getframe().f_back self.reset() while frame: frame.f_trace = self.trace_dispatch self.botframe = frame frame = frame.f_back self.set_step() sys.settrace(self.trace_dispatch)
一顿操作猛如虎,一头雾水不清楚。 (:з」∠)
frame 类型说明
首先需要理解参数输入的 frame 是什么 frame 其实 Python 内置的类,用来描述当前函数域相关的信息。 inspect 模块里面也有 currentFrame 函数获取当前代码域信息。 而上面的操作和 sys 模块的私有函数 _getframe 是一样的。
关于 frame 的类型说明可以参考 python inspect 模块的官方说明 国内还有大佬写了关于 Cpython 里面 frame object 底层的一些生成原理 链接 可惜这个文章太过深奥,我就跳过了。
下面则是一些其他国内文章,内容通俗易懂,可惜缺乏深度↓↓↓
后来在 stack overflow 找到一篇问题 通过文章回答的链接找到了一篇很好的 frame 运行的代码 链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import sysdef one (): two() def two (): three() def three (): for num in range (3 ): frame = sys._getframe(num) show_frame(num, frame) def show_frame (num, frame ): print frame print " frame = sys._getframe(%s)" % num print " function = %s()" % frame.f_code.co_name print " file/line = %s:%s" % (frame.f_code.co_filename, frame.f_lineno) one()
通过 frame 可以很好查找到当前代码运行的信息,所以在 pdb 和 traceback 模块里面都是非常重要的组成元素
sys.settrace 讲解
了解了 frame 之后,我们可以继续测试后续的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 def set_step (self ): """Stop after one line of code.""" if self.frame_returning: caller_frame = self.frame_returning.f_back if caller_frame and not caller_frame.f_trace: caller_frame.f_trace = self.trace_dispatch self._set_stopinfo(None , None ) def _set_stopinfo (self, stopframe, returnframe, stoplineno=0 ): self.stopframe = stopframe self.returnframe = returnframe self.quitting = 0 self.stoplineno = stoplineno def reset (self ): import linecache linecache.checkcache() self.botframe = None self._set_stopinfo(None , None ) self.lineno = None self.stack = [] self.curindex = 0 self.curframe = None def set_trace (self, frame=None ): if frame is None : frame = sys._getframe().f_back self.reset() while frame: frame.f_trace = self.trace_dispatch self.botframe = frame frame = frame.f_back self.set_step() sys.settrace(self.trace_dispatch)
上面我将 set_trace 调用掉的一些函数也贴出来了。 我发现最核心的代码其实最后一行 sys.settrace(self.trace_dispatch)
只有这样在才能触发 断点输入 否则 pdb 不起作用了。
其实我也不太清楚 sys.settrace
函数是怎么运作的 刚好又在 Stack Overflow 上找到了一些参考 链接 根据这个参考可以知道这个函数是一个全局钩子,每个函数调用都会触发到这个函数。 因此通过这个操作可以实现一些 python 黑科技操作。 当然正如回答说的,如果不是为 Debug 的话,使用这个函数会让其他开代码的人非常难受,而且会导致性能下降。 所以我看 pdb 也是看得难受至极了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import sysdef trace_func (frame,event,arg ): value = frame.f_locals["a" ] if value % 2 == 0 : value += 1 frame.f_locals["a" ] = value def f (a ): print a if __name__ == "__main__" : sys.settrace(trace_func) for i in range (0 ,5 ): f(i)
frame 对象里面的 f_trace 属性就相当于 局部钩子函数 链接
trace_dispatch 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def trace_dispatch (self, frame, event, arg ): print "trace_dispatch" ,frame,event,arg if self.quitting: return if event == 'line' : return self.dispatch_line(frame) if event == 'call' : return self.dispatch_call(frame, arg) if event == 'return' : return self.dispatch_return(frame, arg) if event == 'exception' : return self.dispatch_exception(frame, arg) if event == 'c_call' : return self.trace_dispatch if event == 'c_exception' : return self.trace_dispatch if event == 'c_return' : return self.trace_dispatch print 'bdb.Bdb.dispatch: unknown debugging event:' , repr (event) return self.trace_dispatch
设置了 sys.settrace 之后,无论在 Python 进行任何代码操作都会触发钩子,这就为断点调试做好了准备。 可以看到钩子函数会默认传入当前触发 frame 和 事件。
为了更好看清楚这个钩子所做的事情,可以继承 Pdb 对象 然后打印出内部的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pdb import Pdbclass TestPdb (Pdb,object ): def trace_dispatch (self, frame, event, arg ): print "trace_dispatch" ,frame,event,arg return super (TestPdb,self).trace_dispatch(frame, event, arg) if __name__ == "__main__" : def combine (s1,s2 ): s3 = s1 + s2 + s1 s3 = '"' + s3 +'"' return s3 a = "aaa" b = "bbb" TestPdb().set_trace() final = combine(a,b) print final
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 F:\repo\Pdb_test>"C:/Program Files/Autodesk/Maya2017/bin/mayapy.exe" f:/repo/Pdb_test/frame_test.py trace_dispatch <frame object at 0x00000222AF1DE048> line None > f:\repo\pdb_test\frame_test.py(16)<module>() -> final = combine(a,b) (Pdb) s trace_dispatch <frame object at 0x00000222AF243908> call None --Call-- > f:\repo\pdb_test\frame_test.py(9)combine() -> def combine(s1,s2): (Pdb) n trace_dispatch <frame object at 0x00000222AF243908> line None > f:\repo\pdb_test\frame_test.py(10)combine() -> s3 = s1 + s2 + s1 (Pdb) n trace_dispatch <frame object at 0x00000222AF243908> line None > f:\repo\pdb_test\frame_test.py(11)combine() -> s3 = '"' + s3 +'"' (Pdb) n trace_dispatch <frame object at 0x00000222AF243908> line None > f:\repo\pdb_test\frame_test.py(12)combine() -> return s3 (Pdb) n trace_dispatch <frame object at 0x00000222AF243908> return "aaabbbaaa" --Return-- > f:\repo\pdb_test\frame_test.py(12)combine()->'"aaabbbaaa"' -> return s3 (Pdb) l 7 if __name__ == "__main__" : 8 9 def combine(s1,s2): 10 s3 = s1 + s2 + s1 11 s3 = '"' + s3 +'"' 12 -> return s3 13 a = "aaa" 14 b = "bbb" 15 TestPdb().set_trace() 16 final = combine(a,b) 17 print final (Pdb) n trace_dispatch <frame object at 0x00000222AF1DE048> line None > f:\repo\pdb_test\frame_test.py(17)<module>() -> print final (Pdb) c "aaabbbaaa"
从上面的调试信息可以看到
代码运行的 event 是 line
函数触发的 event 是 call
函数返回的 event 是 return | 并且将返回值 到 arg 上
函数报错的 event 是 exception | 同理可以推断
附注: 测试的时候 super 代码忘记前面添加 return 导致无法 step 进调试函数内部
cmd模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def dispatch_line (self, frame ): if self.stop_here(frame) or self.break_here(frame): self.user_line(frame) if self.quitting: raise BdbQuit return self.trace_dispatch def user_line (self, frame ): """This function is called when we stop or break at this line.""" if self._wait_for_mainpyfile: if (self.mainpyfile != self.canonic(frame.f_code.co_filename) or frame.f_lineno<= 0 ): return self._wait_for_mainpyfile = 0 if self.bp_commands(frame): self.interaction(frame, None ) def interaction (self, frame, traceback ): self.setup(frame, traceback) self.print_stack_entry(self.stack[self.curindex]) self.cmdloop() self.forget()
那么可以知道一般情况下运行代码会执行 dispatch_line 会调用 user_line 而其中经过我的测试,触发输入调试的代码是 interaction 里面的 cmdloop
这个 cmdloop 可就是大有来头了,这个和 pdb 模块简称的 Cmd 对象有莫大的关系 Cmd 对象来自内置 cmd 库,用于快速构建命令行工具的库。 使用方法可以参考别人研究的 文章 pdb的输入调用操作是完全基于 cmd 库的标准做的,也难怪我也开始找到一些 help 函数之类的信息却完全找不到调用的地方。 因为这个调用逻辑封装在 Cmd 对象里面了。
因此对应的操作函数其实都在 do_next do_list 这些函数上了。 那么将 cmdloop 替换成函数能否实现 settrace 状态下直接触发想要的调试效果呢? 答案是否定的,直接替换会报错,看了一下上面文章对 Cmd 的运用范例可以知道。 cmd 内部也有相应的执行前和执行后的钩子,pdb模块对执行前做了相应的处理,如果去掉就无法执行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pdb import Pdbclass TestPdb (Pdb,object ): def interaction (self, frame, traceback ): self.setup(frame, traceback) self.print_stack_entry(self.stack[self.curindex]) arg = 'c' self.precmd(arg) self.onecmd(arg) self.forget() if __name__ == "__main__" : def combine (s1,s2 ): s3 = s1 + s2 + s1 s3 = '"' + s3 +'"' return s3 a = "aaa" b = "bbb" TestPdb().set_trace() final = combine(a,b) print final
只要执行上面的代码就可以无条件跳过所有的断点了。 当然,如果将 c 改为 l 罗列断点的话就会陷入死循环了 (:з」∠)
总结
以上就是这次 pdb 研究的情况了,其实还有很多pdb相关的东西没有深入探讨 这次研究的目的是为了移除 pdb 模块的输入等待方式,打算在 Maya 中引入 UI 相应实现动态 Debug 的效果。(mpdb 插件) 目前我已经得到了最核心的输入相应代码,因此见好就收了。
个人觉得 pdb 模块写得非常不友好,注释不太够,太多乱七八糟的参数不明所以,网上讲解的文章也非常少。 还好自己总算是经过重重险阻找到了解决方案。