随着挪动互联网的疾速倒退,人工智能在挪动端上的利用越来越宽泛,团体内端智能在图像识别、视频检测、数据计算等外围场景施展着重要作用。而在开发阶段,Python 毋庸置疑是算法进行研发的首选语言。但在挪动端上,进行算法的部署、调试、验证,仍处在“刀耕火种”的时代,目前算法次要通过在代码中插入日志,验证程序的运行逻辑和后果。
通过打日志当然也能验证后果和定位问题,但一旦工程略微简单点,生产效率会非常低。因而,在 MNN 工作台之中(点击文末浏览原文,返回 MNN 官网:www.mnn.zone 下载)嵌入了端侧 Python 调试能力。常常应用Python的同学肯定相熟pdb模块,它是Python官网规范库提供的交互式代码调试器,和任何一门语言提供的调试能力一样,pdb提供了源代码行级别的设置断点、单步执行等惯例调试能力,是Python开发的一个很重要的工具模块。
明天就让咱们来重点剖析下官网pdb模块源码,看看其调试性能的底层技术原理。
原理
从cpython源码中能够看到,pdb模块并非c实现的内置模块,而是纯Python实现和封装的模块。外围文件是pdb.py,它继承自bdb和cmd模块:
class Pdb(bdb.Bdb, cmd.Cmd): ...
基本原理:利用cmd模块定义和实现一系列的调试命令的交互式输出,基于sys.settrace插桩跟踪代码运行的栈帧,针对不同的调试命令控制代码的运行和断点状态,并向控制台输入对应的信息。
cmd模块次要是提供一个控制台的命令交互能力,通过raw_input/readline这些阻塞的办法实现输出期待,而后将命令交给子类解决决定是否持续循环输出上来,就和他次要的办法名runloop一样。
cmd是一个罕用的模块,并非为pdb专门设计的,pdb应用了cmd的框架从而实现了交互式自定义调试。
bdb提供了调试的外围框架,依赖sys.settrace进行代码的单步运行跟踪,而后散发对应的事件(call/line/return/exception)交给子类(pdb)解决。bdb的外围逻辑在对于调试命令的中断管制,比方输出一个单步运行的”s“命令,决定是否须要持续跟踪运行还是中断期待交互输出,中断到哪一帧等。
根本流程
- pdb启动,以后frame绑定跟踪函数trace_dispatch
def trace_dispatch(self, frame, event, arg): if self.quitting: return # None 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': ...
- 每一帧的不同事件的解决都会通过中断管制逻辑,次要是stop_here(line事件还会通过break_here)函数,解决后决定代码是否中断,须要中断到哪一行
- 如须要中断,触发子类办法user_#event,子类通过interaction实现栈帧信息更新,并在控制台打印对应的信息,而后执行cmdloop让控制台处于期待交互输出
def interaction(self, frame, traceback): self.setup(frame, traceback) # 以后栈、frame、local vars self.print_stack_entry(self.stack[self.curindex]) self.cmdloop() self.forget()
- 用户输出调试命令如“next”并回车,首先会调用set_#命令,对stopframe、returnframe、stoplineno进行设置,它会影响中断管制
`stop_here
的逻辑,从而决定运行到下一帧的中断后果.
def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): self.stopframe = stopframe self.returnframe = returnframe self.quitting = 0 # stoplineno >= 0 means: stop at line >= the stoplineno # stoplineno -1 means: don't stop at all self.stoplineno = stoplineno
- 对于调试过程管制类的命令,个别do_#命令都会返回1,这样本次runloop立马完结,下次运行到某一帧触发中断会再次启动runloop(见步骤3);对于信息获取类的命令,do_#命令都没有返回值,放弃以后的中断状态。
- 代码运行到下一帧,反复步骤3
中断管制
中断管制也就是对于不同的调试命令输出后,能让代码执行到正确的地位进行,期待用户输出,比方输出”s”控制台就应该在下一个运行frame的代码处进行,而输入“c”就须要运行到下一个打断点的中央。中断管制产生在sys.settrace的每一步跟踪的中,是调试运行的外围逻辑。
pdb中次要跟踪了frame的四个事件:
- line:同一个frame中的程序执行事件
- call:产生函数调用,跳到下一级的frame中,在函数第一行产生call事件
- return:函数执行完最初一行(line),产生后果返回,行将跳出以后frame回到上一级frame,在函数最初一行产生return事件
- exception:函数执行中产生异样,在异样行产生exception事件,而后在该行返回(return事件),接下来一级一级向上在frame中产生exception和return事件,直到回到底层frame。
它们是代码跟踪时的不同节点类型,pdb依据用户输出的调试命令,在每一步frame跟踪时都会进行中断管制,决定接下来是否中断,中断到哪一行。中断管制的次要办法是stop_here:
def stop_here(self, frame): # (CT) stopframe may now also be None, see dispatch_call. # (CT) the former test for None is therefore removed from here. if self.skip and \ self.is_skipped_module(frame.f_globals.get('__name__')): return False # next if frame is self.stopframe: # stoplineno >= 0 means: stop at line >= the stoplineno # stoplineno -1 means: don't stop at all if self.stoplineno == -1: return False return frame.f_lineno >= self.stoplineno # step:以后只有追溯到botframe,就期待执行。 while frame is not None and frame is not self.stopframe: if frame is self.botframe: return True frame = frame.f_back return False
调试命令大体上分两类:
- 过程管制:如setp、next、continue等这些执行后马上进入下阶段的代码执行
- 信息获取/设置:如args、p、list等获取以后信息的,也不会影响cmd状态
上面重点解说几个最常见的,用于过程管制的调试命令中断管制实现原理:
s(step)
1 命令定义
执行下一条命令,如果本句是函数调用,则 s 会执行到函数的第一句。
2 代码剖析
pdb中实现逻辑为程序执行每一个帧frame并期待执行,它的执行粒度和settrace一样。
def stop_here(self, frame): ... # stopframe为None if frame is self.stopframe: ... # 以后frame肯定会追溯到botframe,返回true while frame is not None and frame is not self.stopframe: if frame is self.botframe: return True frame = frame.f_back return False
step会将stopframe设置为None,因而只有以后frame能向后始终追溯到底层frame(botframe),就示意能够期待执行了,也就是pdb处于交互期待状态。
因为step的执行粒度和settrace一样,所以运行到每一帧都会期待执行。
n(next)
1 命令定义
执行下一条语句,如果本句是函数调用,则执行函数,接着执行以后执行语句的下一条。
2 代码剖析
pdb中实现逻辑为,运行至以后frame的下一次跟踪中断,但进入到下一个frame(函数调用)中不会中断。
def stop_here(self, frame): ... # 如果frame还没跳出stopframe,永远返回true if frame is self.stopframe: if self.stoplineno == -1: return False return frame.f_lineno >= self.stoplineno # 如果frame跳出了stopframe,进入下一个frame,则执行不会中断,始终到跳出到stopframe # 还有一种状况,如果在return事件中断执行了next,下一次跟踪在上一级frame中,此时上一级frame能跟踪到botframe,中断 while frame is not None and frame is not self.stopframe: if frame is self.botframe: return True frame = frame.f_back return False
next会设置stopframe为以后frame,也就是除非在以后frame内,进入其余的frame都不会执行中断。
c
1 命令定义
继续执行,直到遇到下一条断点
2 代码剖析
stopframe设置为botframe,stoplineno设置为-1。stop_here总返回false,运行不会中断,直到遇到断点(break_here条件成立)
def stop_here(self, frame): ... # 如果在botframe中,stoplineno为-1返回false if frame is self.stopframe: if self.stoplineno == -1: return False return frame.f_lineno >= self.stoplineno # 如果在非botframe中,会先追溯到stopframe,返回false while frame is not None and frame is not self.stopframe: if frame is self.botframe: return True frame = frame.f_back return False
r(return)
1 命令定义
执行以后运行函数到完结。
2 代码剖析
return命令仅在执行到frame完结(函数调用)时中断,也就是遇到return事件时中断。\
pdb会设置stopframe为上一帧frame,returnframe为以后frame。如果是非return事件,stop_here永远返回false,不会中断;
def stop_here(self, frame): ... # 如果以后帧代码程序执行,下一个frame的lineno==stoplineno # 如果执行到for循环的最初一行,下一个frame(for循环第一行)的lineno<stoplineno,不会中断。直到for循环执行完结,紧接着的下一行的lineno==stoplineno,执行中断 if frame is self.stopframe: if self.stoplineno == -1: return False return frame.f_lineno >= self.stoplineno # 如果在非botframe中,会先追溯到stopframe,返回false,同next while frame is not None and frame is not self.stopframe: if frame is self.botframe: return True frame = frame.f_back return False
如果是return事件,stop_here依然返回false,然而returnframe为以后frame判断成立,会执行中断。
def dispatch_return(self, frame, arg): if self.stop_here(frame) or frame == self.returnframe: self.user_return(frame, arg) if self.quitting: raise BdbQuit return self.trace_dispatch
unt(until)
1 命令定义
执行到下一行,和next的区别就在于for循环只会跟踪一次
2 代码剖析
设置stopframe和returnframe为以后frame,stoplineno为以后lineno+1。
def stop_here(self, frame): ... # 如果以后帧代码程序执行,下一个frame的lineno==stoplineno # 如果执行到for循环的最初一行,下一个frame(for循环第一行)的lineno<stoplineno,不会中断。直到for循环执行完结,紧接着的下一行的lineno==stoplineno,执行中断 if frame is self.stopframe: if self.stoplineno == -1: return False return frame.f_lineno >= self.stoplineno # 如果在非botframe中,会先追溯到stopframe,返回false,同next while frame is not None and frame is not self.stopframe: if frame is self.botframe: return True frame = frame.f_back return False
如果在以后frame中有for循环,只会从上向下执行一次。如果是函数返回return事件,下一个frame的lineno有可能小于stoplineno,所以把returnframe设置为以后frame,这样函数执行就和next体现一样了。
u(up)/ d(down)
1 命令定义
切换到上/下一个栈帧
2 代码剖析
栈帧信息
栈帧蕴含代码调用门路上的每一级frame信息,每次命令执行中断都会刷新,能够通过u/d命令高低切换frame。\
栈帧获取次要通过get_stack办法,第一个参数是frame,第二个参数是traceback object。traceback object是在exception事件产生的,exception事件会带一个arg参数:
exc_type, exc_value, exc_traceback = arg(<type 'exceptions.IOError'>, (2, 'No such file or directory', 'wdwrg'), <traceback object at 0x10bd08a70>)
traceback object有几个罕用的属性:
- tb_frame:以后exception产生在的frame
- tb_lineno:以后exception产生在的frame的行号,即frame.tb_lineno
- tb_next:指向堆栈下一级调用的exc_traceback(traceback object),如果是最顶层则为None
栈帧信息由两局部组成,frame的调用栈和异样栈(如有),程序为:botframe -> frame1 -> frame2 -> tb1 -> tb2(出错tb)
def get_stack(self, f, t): stack = [] if t and t.tb_frame is f: t = t.tb_next # frame调用栈,从底到顶 while f is not None: stack.append((f, f.f_lineno)) if f is self.botframe: break f = f.f_back stack.reverse() i = max(0, len(stack) - 1) # 异样栈,从底到顶(出错栈) while t is not None: stack.append((t.tb_frame, t.tb_lineno)) t = t.tb_next if f is None: i = max(0, len(stack) - 1) return stack, i
pdb每次执行中断都会更新调用的栈帧表,以及以后的栈帧信息,堆栈切换只有向上/下切换索引即可。
def setup(self, f, t): self.forget() self.stack, self.curindex = self.get_stack(f, t) self.curframe_locals = self.curframe.f_locals ......def do_up(self, arg): if self.curindex == 0: print >>self.stdout, '*** Oldest frame' else: self.curindex = self.curindex - 1 self.curframe = self.stack[self.curindex][0] self.curframe_locals = self.curframe.f_locals self.print_stack_entry(self.stack[self.curindex]) self.lineno = None
b(break)
区别于过程管制的调试命令,break命令用来设置断点,不会马上影响程序中断状态,但可能会影响后续的中断。在line事件产生的时候,除了stop_here会减少break_here的条件判断,设置断点的实现比较简单,这里次要介绍对函数设置断点的时候,是怎么让代码执行到函数第一行中断的。
设置断点时,断点的lineno为了函数的第一行:
# 函数断点示例:break funcdef do_break(self, arg, temporary = 0): ... if hasattr(func, 'im_func'): func = func.im_func funcname = code.co_name lineno = code.co_firstlineno filename = code.co_filename
当line事件执行到函数的第一行代码时,这一行没有被动设置过断点,然而函数第一行co_firstlineno命中断点,所以会持续判断断点有效性。
def break_here(self, frame): ... lineno = frame.f_lineno if not lineno in self.breaks[filename]: lineno = frame.f_code.co_firstlineno if not lineno in self.breaks[filename]: return False # flag says ok to delete temp. bp (bp, flag) = effective(filename, lineno, frame)
断点的有效性判断通过effective办法,其中解决了ignore、enabled这些配置,对函数断点的有效性判断通过checkfuncname办法:
def checkfuncname(b, frame): """Check whether we should break here because of `b.funcname`.""" ... # Breakpoint set via function name. ... # We are in the right frame. if not b.func_first_executable_line: # The function is entered for the 1st time. b.func_first_executable_line = frame.f_lineno if b.func_first_executable_line != frame.f_lineno: # But we are not at the first line number: don't break. return False return True
在line事件在函数第一行产生时,func_first_executable_line还没有,于是设置为以后行号,并且断点失效,因而函数执行到第一行中断。接下来line到行数的前面行时,因为func_first_executable_line曾经有值,并且必定不等于以后行号,所以break_here判断为有效,不会中断。
实例剖析
以下联合一个很简略的Python代码调试的例子,温习下上述命令的实现原理:
在控制台中,命令行执行快照:
命令行中执行python test.py,Python代码理论是从第一行开始执行的,但因为pdb.set_trace()是在__main__中调用的,所以理论是从set_trace的下一行才挂载到pdb的跟踪函数,开始frame的中断管制。
这段Python代码执行会通过通过3个frame:
- 底层根frame0,即__main__所在的frame0,其中蕴含一断for循环代码,frame0的back frame为None
- 第二层frame1,进入func办法所在的frame1,frame1的back frame为frame0
- 顶层frame2,进入add办法所在的frame2,frame2的back frame为frame1
调试过程:
- 跟踪__main__所在的frame(根frame0),在20行触发line事件
- 用户输出unt命令回车,frame0在21行触发line事件,行号等于上一次跟踪行号+1,stop_here成立,中断期待
- 用户输出unt命令回车,同2,在22行中断
- 用户输出unt命令回车,代码跟踪至frame0在20行触发line事件,行号小于上一次跟踪行号+1(23),stop_here不成立,继续执行
- 在24行触发line事件,行号大于上一次跟踪行号+1(23),stop_here成立,中断期待
- 用户输出s命令回车,代码跟踪至frame1在12行触发call事件,step执行粒度和sys.settrace一样,在12行中断期待
- 用户设置add函数断点,断点列表中会退出add函数的第一行(第7行)的断点
- 用户输出c命令回车,stop_here总返回false,持续跟踪运行直到在第8行触发line事件,尽管第8行不再断点列表中,但以后函数帧firstlineno在,并且无效,所以在第8行中断期待
- 用户输出r命令回车,前面的line事件处理中stop_here都返回false,直到在第10行触发return事件,此时returnframe为以后frame,在10行中断期待
- 用户输出up命令,栈帧向前切换索引,回到上一帧frame1,也就是第13行func中调用add的中央
- 用户输出down命令,栈帧向前后切换索引,回到以后帧
- 用户输出n命令,运行至下一次跟踪14行(line事件),这一次跟踪在frame1上,能追溯到botframe,所以在14行中断
- 用户输出n命令,运行至下一次跟踪14行(return事件),还在以后frame1中,中断
- 用户输出n命令,运行至下一次跟踪24行(return事件),这一次跟踪就是botframe(frame0),中断
- 用户输出n命令,frame0执行完结。
小结
Python规范库提供的pdb的实现并不简单,本文对源码中的外围的逻辑做了解说,如果你理解其原理,也能够本人定制或重写一个Python调试器。事实上,业界的很多通用IDE如pycharm、vscode等都没有应用规范的pdb,他们开发了本人的Python调试器来更好的适配IDE。不过理解pdb原理,在pdb上改写和定制调试器来满足调试需要,也是一种成本低而无效的形式。
MNN工作台对端侧的调试能力也是基于原生pdb实现的,并且反对阿里巴巴团体内端计算的各种研发场景,对算法的研发部署都有很大的效率晋升。点击浏览原文,返回 www.mnn.zone 下载 MNN 工作台赶快体验吧。
关注咱们,每周 3 篇挪动技术实际&干货给你思考!