Python Pdb源码解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 经常使用Python的同学一定熟悉pdb模块,它是Python官方标准库提供的交互式代码调试器,和任何一门语言提供的调试能力一样,pdb提供了源代码行级别的设置断点、单步执行等常规调试能力,是Python开发的一个很重要的工具模块。pdb使用方法见官方文档,本文重点分析官方pdb模块源码,介绍调试功能的实现原理。

原创 鹿尤 淘系技术  6月18日


640.gif


经常使用Python的同学一定熟悉pdb模块,它是Python官方标准库提供的交互式代码调试器,和任何一门语言提供的调试能力一样,pdb提供了源代码行级别的设置断点、单步执行等常规调试能力,是Python开发的一个很重要的工具模块。

pdb使用方法见官方文档,本文重点分析官方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(见第三点);对于信息获取类的命令,do_#命令都没有返回值,保持当前的中断状态。


  • 代码运行到下一帧,重复第三点


中断控制


中断控制也就是对于不同的调试命令输入后,能让代码执行到正确的位置停止,等待用户输入,比如输入”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


调试命令大体上分两类:


  1. 过程控制:如setp、next、continue等这些执行后马上进入下阶段的代码执行
  2. 信息获取/设置:如args、p、list等获取当前信息的,也不会影响cmd状态


以下重点讲解几个最常见的用于过程控制的调试命令的中断控制实现原理:


  s(step)

  • 命令定义


执行下一条命令,如果本句是函数调用,则 s 会执行到函数的第一句。


  • 代码分析


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)


  • 命令定义


执行下一条语句,如果本句是函数调用,则执行函数,接着执行当前执行语句的下一条。


  • 代码分析


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


  • 命令定义


继续执行,直到遇到下一条断点


  • 代码分析


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)


  • 命令定义


执行当前运行函数到结束。


  • 代码分析


return命令仅在执行到frame结束(函数调用)时中断,也就是遇到return事件时中断。


pdb会设置stopframe为上一帧frame,returnframe为当前frame。如果是非return事件,stop_here永远返回false,不会中断;



def stop_here(self, frame):
        ...
        # 会先追溯到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


如果是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)


  • 命令定义


执行到下一行,和next的区别就在于for循环只会跟踪一次


  • 代码分析


设置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)

  • 命令定义


切换到上/下一个栈帧


  • 代码分析


栈帧信息


栈帧包含代码调用路径上的每一级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 func
def 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代码调试的例子,复习下上述命令的实现原理:

image.png


在控制台中,命令行执行快照:


image.png


命令行中执行Python test.py,Python代码实际是从第一行开始执行的,但因为pdb.set_trace()是在__main__中调用的,所以实际是从set_trace的下一行才挂载到pdb的跟踪函数,开始frame的中断控制。


段Python代码执行会经过经过3个frame:


  1. 底层根frame0,即_main_所在的frame0,其中包含一断for循环代码,frame0的back frame为None
  2. 第二层frame1,进入func方法所在的frame1,frame1的back frame为frame0
  3. 顶层frame2,进入add方法所在的frame2,frame2的back frame为frame1


调试过程:


  1. 跟踪_main_所在的frame(根frame0),在20行触发line事件
  2. 用户输入unt命令回车,frame0在21行触发line事件,行号等于上一次跟踪行号+1,stop_here成立,中断等待
  3. 用户输入unt命令回车,同2,在22行中断
  4. 用户输入unt命令回车,代码跟踪至frame0在20行触发line事件,行号小于上一次跟踪行号+1(23),stop_here不成立,继续执行
  5. 在24行触发line事件,行号大于上一次跟踪行号+1(23),stop_here成立,中断等待
  6. 用户输入s命令回车,代码跟踪至frame1在12行触发call事件,step执行粒度和sys.settrace一样,在12行中断等待
  7. 用户设置add函数断点,断点列表中会加入add函数的第一行(第7行)的断点
  8. 用户输入c命令回车,stop_here总返回false,继续跟踪运行直到在第8行触发line事件,虽然第8行不再断点列表中,但当前函数帧firstlineno在,并且有效,所以在第8行中断等待
  9. 用户输入r命令回车,后面的line事件处理中stop_here都返回false,直到在第10行触发return事件,此时returnframe为当前frame,在10行中断等待
  10. 用户输入up命令,栈帧向前切换索引,回到上一帧frame1,也就是第13行func中调用add的地方
  11. 用户输入down命令,栈帧向前后切换索引,回到当前帧
  12. 用户输入n命令,运行至下一次跟踪14行(line事件),这一次跟踪在frame1上,能追溯到botframe,所以在14行中断
  13. 用户输入n命令,运行至下一次跟踪14行(return事件),还在当前frame1中,中断
  14. 用户输入n命令,运行至下一次跟踪24行(return事件),这一次跟踪就是botframe(frame0),中断
  15. 用户输入n命令,frame0执行结束。


小结


Python标准库提供的pdb的实现并不复杂,本文对源码中的核心的逻辑做了讲解,如果你了解其原理,也可以自己定制或重写一个Python调试器。


事实上,业界的很多通用IDE如pycharm、vscode等都没有使用标准的pdb,他们开发了自己的Python调试器来更好的适配IDE。


不过了解pdb原理,在pdb上改写和定制调试器来满足调试需求,也是一种成本低而有效的方式。

相关文章
|
28天前
|
IDE 测试技术 开发工具
10个必备Python调试技巧:从pdb到单元测试的开发效率提升指南
在Python开发中,调试是提升效率的关键技能。本文总结了10个实用的调试方法,涵盖内置调试器pdb、breakpoint()函数、断言机制、logging模块、列表推导式优化、IPython调试、警告机制、IDE调试工具、inspect模块和单元测试框架的应用。通过这些技巧,开发者可以更高效地定位和解决问题,提高代码质量。
186 8
10个必备Python调试技巧:从pdb到单元测试的开发效率提升指南
|
27天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
27天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
27天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
13天前
|
数据采集 供应链 API
Python爬虫与1688图片搜索API接口:深度解析与显著收益
在电子商务领域,数据是驱动业务决策的核心。阿里巴巴旗下的1688平台作为全球领先的B2B市场,提供了丰富的API接口,特别是图片搜索API(`item_search_img`),允许开发者通过上传图片搜索相似商品。本文介绍如何结合Python爬虫技术高效利用该接口,提升搜索效率和用户体验,助力企业实现自动化商品搜索、库存管理优化、竞品监控与定价策略调整等,显著提高运营效率和市场竞争力。
44 3
|
3天前
|
自然语言处理 数据处理 索引
mindspeed-llm源码解析(一)preprocess_data
mindspeed-llm是昇腾模型套件代码仓,原来叫"modelLink"。这篇文章带大家阅读一下数据处理脚本preprocess_data.py(基于1.0.0分支),数据处理是模型训练的第一步,经常会用到。
13 0
|
1月前
|
数据挖掘 vr&ar C++
让UE自动运行Python脚本:实现与实例解析
本文介绍如何配置Unreal Engine(UE)以自动运行Python脚本,提高开发效率。通过安装Python、配置UE环境及使用第三方插件,实现Python与UE的集成。结合蓝图和C++示例,展示自动化任务处理、关卡生成及数据分析等应用场景。
117 5
|
28天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
Linux C语言 开发者
源码安装Python学会有用还能装逼 | 解决各种坑
相信朋友们都看过这个零基础学习Python的开篇了
469 0
源码安装Python学会有用还能装逼 | 解决各种坑

热门文章

最新文章