• 从pdb源码到frame帧对象


    a18c41c2a406e896147e437038479eda.png

    前言

    在使用pdb对某Python程序进行debug时,出现通过l或ll命令,无法获得代码上下文的情况,如下图:

    175dcd7c51dbf44f0b40808cd221759e.png

    所以我决定深究一下pdb代码是怎么写的,为啥有时候获取不到上下文代码。

    最小实例

    pdb是Python内置的调试器,其源码由Python实现,基于cmd和bdb这两个内置库实现,多数情况下,pdb还是很好用的,虽说如此,但PyCharm、Vscode这些都没有使用标准的pdb,而是自己开发了Python调试器来配合IDE。

    为了直观理解pdb运行流程,这里构建一下最小实例,将pdb运行起来:

    1. import pdb
    2. def fib(n):
    3.     a, b = 11
    4.     # 下断点
    5.     pdb.set_trace()
    6.     for i in range(n - 1):
    7.         a, b = b, a + b
    8.     return a
    9. fib(10)

    我在pycharm中运行上面代码,然后debug起来。

    在调用pdb.set_trace()方法时,第一步便是实例化pdb对象:

    1. def set_trace(*, header=None):
    2.     # 实例化
    3.     pdb = Pdb()
    4.     if header is not None:
    5.         pdb.message(header)
    6.     pdb.set_trace(sys._getframe().f_back)

    实例化会调用__init__方法:

    1. class Pdb(bdb.Bdb, cmd.Cmd):
    2.     _previous_sigint_handler = None
    3.     def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
    4.                  nosigint=False, readrc=True):
    5.         bdb.Bdb.__init__(self, skip=skip)
    6.         cmd.Cmd.__init__(self, completekey, stdin, stdout)
    7.         sys.audit("pdb.Pdb")
    8.         # ... 省略

    从Pdb类可知,Pdb继承了bdb和cmd。

    bdb内置模块是Python提供调试能力的核心框架,它基于sys.setrace方法提供的动态插桩能力,实现对代码的单步调试。而cmd模块主要用于实现交互式命令的,是常用模块,并不是为pdb专门设计的。

    先从简单的cmd开始讨论。

    cmd是Python内置的模块,主要用于实现交互式shell,我们可以基于cmd轻松实现一个自己的交互式shell,这里简单演示一下cmd的使用(因为不是本文重点,便不去深究了):

    1. from cmd import Cmd
    2. class MyCmd(Cmd):
    3.     def __init__(self):
    4.         Cmd.__init__(self)
    5.     def do_name(self, name):
    6.         print(f'Hello, {name}')
    7.     def do_exit(self, arg):
    8.         print('Bye!')
    9.         return True
    10. if __name__ == '__main__':
    11.     mycmd = MyCmd()
    12.     mycmd.cmdloop()

    上述代码中,定义了MyCmd类,继承于Cmd类,然后实现了do_name方法和do_exit方法,这两个方法分别会匹配上name命令和exit命令,然后通过cmdloop方法开始运mycmd,效果如下:

    7d9f78ee0293a3f260b7eb54908a64f8.png

    frame栈帧对象

    回顾一下set_trace方法:

    1. def set_trace(*, header=None):
    2.     pdb = Pdb()
    3.     if header is not None:
    4.         pdb.message(header)
    5.     pdb.set_trace(sys._getframe().f_back)

    实例化完后,会通过sys._getframe().f_back获得frame对象,然后传递给pdb.set_trace方法。

    其中sys._getframe()方法会获得当前的frame(栈帧)。

    当我们运行Python代码时,解释器会创建相应的PyFrameObject对象(即上面我们说的frame)。从Python源码中,我们可以翻出PyFrameObject的定义,如下:

    1. typedef struct _frame {
    2.     PyObject_VAR_HEAD
    3.     struct _frame *f_back;      /* previous frame, or NULL */
    4.     PyCodeObject *f_code;       /* code segment */
    5.     PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
    6.     PyObject *f_globals;        /* global symbol table (PyDictObject) */
    7.     PyObject *f_locals;         /* local symbol table (any mapping) */
    8.     PyObject **f_valuestack;    /* points after the last local */
    9.     /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
    10.        Frame evaluation usually NULLs it, but a frame that yields sets it
    11.        to the current stack top. */
    12.     PyObject **f_stacktop;
    13.     ...
    14.     int f_lasti;                /* Last instruction if called */
    15.     /* Call PyFrame_GetLineNumber() instead of reading this field
    16.        directly.  As of 2.3 f_lineno is only valid when tracing is
    17.        active (i.e. when f_trace is set).  At other times we use
    18.        PyCode_Addr2Line to calculate the line from the current
    19.        bytecode index. */
    20.     int f_lineno;               /* Current line number */
    21.     int f_iblock;               /* index in f_blockstack */
    22.     char f_executing;           /* whether the frame is still executing */
    23.     PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    24.     PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
    25. } PyFrameObject;

    Python实际执行中,会产生很多PyFrameObject对象,这些对象会链接起来,构成执行链表,解释器训练处理链表上的栈帧对象,处理时就入栈,处理完便出栈。

    通过PyFrameObject定义代码中的注释可知:

    • f_back:获得执行环境链表中的上一个栈帧,使新的栈帧在结束后还能回到旧的栈帧对象中。

    • f_code:存放PyCodeObject对象

    • f_builtins、f_globals、f_locals:符号表对象(字典类型)

    • f_valuestack:运行时栈的栈底(解释器会循环处理执行环境链表中的frame,将其入栈处理,f_valuestack指向这个栈的栈底)

    • f_stacktop:运行时栈的栈顶

    • f_lasti:当前以及执行过的字节码指令的位置

    • f_lineno:当前执行的PyCodeObject对象的行号

    在Python中可以查看一下上面的属性:

    1. In [1]: import sys
    2. In [2]: frame = sys._getframe()
    3. In [3]: frame
    4. Out[3]: <frame at 0x000001A8F2874850, file '<ipython-input-2-8c3916095986>', line 1, code <cell line: 1>>
    5. In [4]: frame.f_back
    6. Out[4]: <frame at 0x000001A8F384C620, file 'C:\\ProgramData\\Anaconda3\\lib\\site-packages\\IPython\\core\\interactiveshell.py', line 3389, code run_code>
    7. In [5]: frame.f_code
    8. Out[5]: <code object <cell line: 1> at 0x000001A8F0933190, file "<ipython-input-2-8c3916095986>", line 1>

    PyFrameObject是可变长对象,即占用的内存大小是动态分配的,到底要分配多少,主要取决于PyFrameObject对象关联的PyCodeObject对象,PyCodeObject对象对应着具体的代码。

    通过dis内置库,我们可以将PyCodeObject对象反编译为指令码(人类可读的),如下:

    1. (Pdb) l
    2.   2
    3.   3
    4.   4     def fib(n):
    5.   5         a, b = 11
    6.   6         pdb.set_trace()
    7.   7  ->     for i in range(n - 1):
    8.   8             a, b = b, a + b
    9.   9
    10.  10         return a
    11.  11
    12.  12
    13. (Pdb) import sys
    14. (Pdb) sys._getframe()
    15. <frame at 0x000001CF3758FCC0, file '<stdin>', line 1, code <module>>
    16. (Pdb) frame = sys._getframe()
    17. (Pdb) frame.f_code
    18. <code object <module> at 0x000001CF38ECDDF0, file "<stdin>", line 1>
    19. (Pdb) frame.f_code.co_code
    20. b'e\x00\xa0\x01\xa1\x00Z\x02d\x00S\x00'
    21. (Pdb) import dis
    22. (Pdb) dis.dis(frame.f_code)
    23.   1           0 LOAD_NAME                0 (sys)
    24.               2 LOAD_METHOD              1 (_getframe)
    25.               4 CALL_METHOD              0
    26.               6 STORE_NAME               2 (frame)
    27.               8 LOAD_CONST               0 (None)
    28.              10 RETURN_VALUE

    上述代码中,通过sys._getframe()获得当前frame,然后通过frame的f_code获得PyCodeObject对象,然后利用dis.dis方法将f_code的内容反编译成指令码。

    sys.settrace方法

    经调试,当我们自己调用pdb.set_trace时,最终会来到bdb的set_trace方法,代码如下:

    1. def set_trace(self, frame=None):
    2.     """Start debugging from frame.
    3.         If frame is not specified, debugging starts from caller's frame.
    4.         """
    5.     if frame is None:
    6.         frame = sys._getframe().f_back
    7.         self.reset()
    8.         while frame:
    9.             frame.f_trace = self.trace_dispatch
    10.             self.botframe = frame
    11.             frame = frame.f_back
    12.             self.set_step()
    13.             sys.settrace(self.trace_dispatch)

    其中的sys.setrace方法便是关键,前文也提了,bdb模块正是基于sys.setrace方法提供的动态插桩能力之上实现代码单步调试的。看一下Python文档对sys.setrace方法的解释:

    sys.settrace(tracefunc) 设置系统的跟踪函数,使得用户在 Python 中就可以实现 Python 源代码调试器。

    该函数是特定于单个线程的,所以要让调试器支持多线程,必须为正在调试的每个线程都用 settrace() 注册一个跟踪函数,或使用 threading.settrace()。跟踪函数应接收三个参数:frame、event 和 arg。 

    更多参考:https://roohui.com/help/tutorial/python_3_9/library/sys.html#sys.settrace

    为了理解sys.setrace方法,先简单使用一下:

    1. import sys
    2. import inspect
    3. class Tracer:
    4.     def trace(self, frame, event, arg):
    5.         # PyCodeObject
    6.         code = frame.f_code
    7.         module = inspect.getmodule(code)
    8.         # module路径
    9.         module_path = module.__file__
    10.         # module名
    11.         module_name = module.__name__
    12.         print(f'event: {event} '
    13.               f'module_path: {module_path} '
    14.               f'module_name: {module_name} '
    15.               f'code_name: {code.co_name} '
    16.               f'lineno: {frame.f_lineno} '
    17.               f'locals: {frame.f_locals} '
    18.               f'args: {arg}')
    19.         return self.trace
    20.     def collect(self, func, *args):
    21.         sys.settrace(self.trace)
    22.         func(*args)
    23. def add(a, b):
    24.     c = 3
    25.     d = 4
    26.     e = c + d
    27.     return a + b + e
    28. if __name__ == '__main__':
    29.     t = Tracer()
    30.     t.collect(add, 12)

    上述代码中,在Tracer类的collect方法中使用了sys.settrace方法,传入了trace方法作为跟踪函数,trace方法必须有frame, event, arg三个参数,其含义如下:

    • frame:当前栈帧

    • event:事件类型,是str类型,有call、line、return等类型

    • arg:与event有关

    运行上述代码,得如下结果:

    1. event: call module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 31 locals: {'a'1'b'2} args: None
    2. event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 32 locals: {'a'1'b'2} args: None
    3. event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 33 locals: {'a'1'b'2'c'3} args: None
    4. event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 34 locals: {'a'1'b'2'c'3'd'4} args: None
    5. event: line module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 35 locals: {'a'1'b'2'c'3'd'4'e'7} args: None
    6. event: return module_path: C:/Users/admin/workplace/play_py/play_tracer.py module_name: __main__ code_name: add lineno: 35 locals: {'a'1'b'2'c'3'd'4'e'7} args: 10

    从这个例子可知,sys.setrace方法可以在Python每次执行时都调用跟踪方法(即上面的trace方法)。这个方法很有意思,后面的文章,还会深入到其c源码进行分析。

    pdb基本流程

    回到pdb,当我们通过pdb.set_trace()启动pdb调试时,会来到bdb的set_trace方法,该方法会通过sys.settrace方法实现单步调用,其传入的跟踪方法为bdb中的trace_dispatch方法,代码如下:

    1. def trace_dispatch(self, frame, event, arg):
    2.         """
    3.         根据不同的event调用不同的跟踪方法(trace function)实现debug
    4.         """
    5.         if self.quitting:
    6.             return # None
    7.         if event == 'line':
    8.             return self.dispatch_line(frame)
    9.         if event == 'call':
    10.             return self.dispatch_call(frame, arg)
    11.         if event == 'return':
    12.             return self.dispatch_return(frame, arg)
    13.         if event == 'exception':
    14.             return self.dispatch_exception(frame, arg)
    15.         if event == 'c_call':
    16.             return self.trace_dispatch
    17.         if event == 'c_exception':
    18.             return self.trace_dispatch
    19.         if event == 'c_return':
    20.             return self.trace_dispatch
    21.         print('bdb.Bdb.dispatch: unknown debugging event:', repr(event))
    22.         return self.trace_dispatch

    从代码可知,trace_dispatch方法针对sys.settrace不同的事件类型调用了不同的处理函数,通过Python文档,可以轻松知道这些事件的含义:

    • call:调用函数时会触发call事件

    • line:解释器执行新一行代码时,会触发该事件

    • return:从函数返回时,会触发该事件

    • exception:发生异常时,会触发该事件

    • opcode:执行新的操作码时会触发该事件

    当然,我们可以通过继承改写的方式,为trace_dispatch方法加上一些输出,可以更直观的理解这些事件,代码如下:

    1. from pdb import Pdb
    2. class MyPdb(Pdb):
    3.     def trace_dispatch(self, frame, event, arg):
    4.         # 加了一行打印
    5.         print(f"[trace_dispatch] frame: {frame}, event: {event}, arg: {arg}")
    6.         return super(MyPdb, self).trace_dispatch(frame, event, arg)
    7. def func(a, b):
    8.     c = a + b
    9.     e = 6
    10.     return c + e
    11. if __name__ == '__main__':
    12.     MyPdb().set_trace()
    13.     func(1, 2)

    代码很简单,就是加了一行print输出,然后我们启动程序,在pdb交互式环境中,进行如下操作:

    1. # 查看断点位置
    2. (Pdb) l
    3.  13       return c + e
    4.  14   
    5.  15   
    6.  16   if __name__ == '__main__':
    7.  17       MyPdb().set_trace()
    8.  18  ->     func(1, 2)
    9. [EOF]
    10. # 下一行,如果是函数,则进入函数内容,从输出看,事件为call
    11. (Pdb) s
    12. [trace_dispatch] frame: <frame at 0x0000011DA363F3A0, file 'C:/Users/admin/workplace/play_py/paly_mypdb.py', line 10, code func>, eventcallargNone
    13. --Call--
    14. > c:\users\admin\workplace\play_py\paly_mypdb.py(10)func()
    15. -> def func(a, b):
    16. # 下一行,如果是函数,不进入,事件是line
    17. (Pdb) n
    18. [trace_dispatch] frame: <frame at 0x0000011DA363F3A0, file 'C:/Users/admin/workplace/play_py/paly_mypdb.py', line 11, code func>, eventlineargNone
    19. > c:\users\admin\workplace\play_py\paly_mypdb.py(11)func()
    20. -> c = a + b

    通过s命令与n命令,我们直观地确认了call与line事件被触发时的情况,也确认了trace_dispatch作为跟踪函数的作用,我们接着深入进去。

    如果我们敲击了n命令,代码会执行到dispatch_line方法(trace_dispatch方法判断触发的是line事件,从而调用dispatch_line方法),其代码如下:

    1. def dispatch_line(self, frame):
    2.     """Invoke user function and return trace function for line event.
    3.     """
    4.     if self.stop_here(frame) or self.break_here(frame):
    5.         self.user_line(frame)
    6.         if self.quitting: raise BdbQuit
    7.         return self.trace_dispatch

    在dispatch_line方法中,会通过stop_here方法与break_here方法判断是否需要中断,中断在哪一行,如果要中断,则调用user_line方法,其他dispatch_#event函数结构类似,都是判断是否要中断,然后调用user_#event函数。以dispatch_call方法验证一下:

    1. def dispatch_call(self, frame, arg):
    2.         
    3.         # XXX 'arg' is no longer used
    4.         if self.botframe is None:
    5.             # First call of dispatch since reset()
    6.             self.botframe = frame.f_back # (CT) Note that this may also be None!
    7.             return self.trace_dispatch
    8.         if not (self.stop_here(frame) or self.break_anywhere(frame)):
    9.             # No need to trace this function
    10.             return # None
    11.         # Ignore call events in generator except when stepping.
    12.         if self.stopframe and frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS:
    13.             return self.trace_dispatch
    14.         self.user_call(frame, arg)
    15.         if self.quitting: raise BdbQuit
    16.         return self.trace_dispatch

    在dispatch_call方法中,如果需要处理,则调用user_call方法。

    看回dispatch_line方法中使用的user_line方法,代码如下:

    1. def user_line(self, frame):
    2.     """This function is called when we stop or break at this line."""
    3.     if self._wait_for_mainpyfile:
    4.         if (self.mainpyfile != self.canonic(frame.f_code.co_filename)
    5.             or frame.f_lineno <= 0):
    6.             return
    7.         self._wait_for_mainpyfile = False
    8.         if self.bp_commands(frame):
    9.             self.interaction(frame, None)

    user_line方法会执行interaction方法,实现frame信息的更新并在控制台打印调用栈中当前栈帧的信息,然后通过_cmdloop方法阻塞,等待用户的命令输入,代码如下:

    1. def interaction(self, frame, traceback):
    2.         # Restore the previous signal handler at the Pdb prompt.
    3.         if Pdb._previous_sigint_handler:
    4.             try:
    5.                 signal.signal(signal.SIGINT, Pdb._previous_sigint_handler)
    6.             except ValueError:  # ValueError: signal only works in main thread
    7.                 pass
    8.             else:
    9.                 Pdb._previous_sigint_handler = None
    10.         # 更新frame信息
    11.         if self.setup(frame, traceback):
    12.             # no interaction desired at this time (happens if .pdbrc contains
    13.             # a command like "continue")
    14.             self.forget()
    15.             return
    16.         # 打印当前栈帧信息
    17.         self.print_stack_entry(self.stack[self.curindex])
    18.         # 等待用户输出
    19.         self._cmdloop()
    20.         # 清理行号、调用栈等信息
    21.         self.forget()

    至此,pdb调试Python程序时,pdb内部的执行流程就清晰了,路径如下:

    pdb.set_trace() -> sys.settrace() -> dispatch_trace() -> dispatch_line() -> interaction() -> _cmdloop()

    回顾上文中,关于cmd的讨论,我们定义了do_name方法、do_exit方法匹配name命令与exit命令,那pdb是否也一样呢?

    答案是:一样的,Pdb类中有很多 do_#命令 方法,分别对于这我们的pdb命令,如下图:

    b972d84e86399813ef23909ea091bf26.png

    看到do_l与do_ll方法,我眼前一亮,因为我看pdb源码的目的就是为了搞清楚本文一开始的问题,为啥pdb获得不到某Python程序中的源码。

    pdb各种命令的源码解析

    l(list)命令

    l命令主要用于查询当前断点周围的源代码,默认是周围11行,来看看它是怎么获得的:

    1. def do_list(self, arg):
    2.         # ... 省略
    3.         
    4.         # 从当前栈帧中获得文件名
    5.         filename = self.curframe.f_code.co_filename
    6.         breaklist = self.get_file_breaks(filename)
    7.         try:
    8.             # 通过getlines方法获得
    9.             lines = linecache.getlines(filename, self.curframe.f_globals)
    10.             self._print_lines(lines[first-1:last], first, breaklist,
    11.                               self.curframe)
    12.             self.lineno = min(last, len(lines))
    13.             if len(lines) < last:
    14.                 self.message('[EOF]')
    15.         except KeyboardInterrupt:
    16.             pass
    17.     do_l = do_list

    我删了do_list方法中处理arg的逻辑,主要就是计算你需要获得周围多少行的源码。

    do_list方法首先会从当前栈帧对象中获得当前文件路径,然后通过getlines方法获得源代码。

    getlines方法中有缓存的逻辑,第一次获取源代码会通过tokenize.open方法去打开源文件,并读取其中的代码,然后再将代码存入缓存,其关键代码在linecache.py的updatecache方法中,代码如下:

    1. def updatecache(filename, module_globals=None):
    2.     # ... 省略不重要代码
    3.     try:
    4.         print('ayu fullname: ', fullname)
    5.         with tokenize.open(fullname) as fp:
    6.             lines = fp.readlines()
    7.             print('ayu lines: ',  lines)
    8.     except OSError:
    9.         return []
    10.     if lines and not lines[-1].endswith('\n'):
    11.         lines[-1] += '\n'
    12.     size, mtime = stat.st_size, stat.st_mtime
    13.     cache[filename] = size, mtime, lines, fullname
    14.     return lines

    为了验证一下,我依旧通过继承的方式重写了updatecache方法,加了一些print,效果如下:

    1. (Pdb) l
    2. ayu fullname:  C:/Users/admin/workplace/play_py/paly_mypdb.py
    3. ayu lines:  ['from pdb import Pdb\n''\n''\n''class MyPdb(Pdb):\n''    def trace_dispatch(self, frame, event, arg):\n''        print(f"[trace_dispatch] frame: {frame}, event: {event}, arg: {arg}")\n''        return super(MyPdb, self).trace_dispatch(frame, event, arg)\n''\n''\n''def func(a, b):\n''    c = a + b\n''    e = 6\n''    return c + e\n''\n''\n'"if __name__ == '__main__':\n"'    MyPdb().set_trace()\n''    func(1, 2)\n']
    4.  13       return c + e
    5.  14   
    6.  15   
    7.  16   if __name__ == '__main__':
    8.  17       MyPdb().set_trace()
    9.  18  ->     func(1, 2)
    10. [EOF]
    11. (Pdb)

    ll(longlist)命令

    ll命令可以获得当前文件所有源码,代码如下:

    1. def do_longlist(self, arg):
    2.         """longlist | ll
    3.         List the whole source code for the current function or frame.
    4.         """
    5.         filename = self.curframe.f_code.co_filename
    6.         breaklist = self.get_file_breaks(filename)
    7.         try:
    8.             # 获得文件中所有代码
    9.             lines, lineno = getsourcelines(self.curframe)
    10.         except OSError as err:
    11.             self.error(err)
    12.             return
    13.         self._print_lines(lines, lineno, breaklist, self.curframe)
    14.     do_ll = do_longlist

    主要通过getsourcelines方法实现这样的效果,而getsourcelines方法主要利用inspect内置模块来获取信息,代码如下:

    1. def getsourcelines(obj):
    2.     lines, lineno = inspect.findsource(obj)
    3.     if inspect.isframe(obj) and obj.f_globals is obj.f_locals:
    4.         # must be a module frame: do not try to cut a block out of it
    5.         return lines, 1
    6.     elif inspect.ismodule(obj):
    7.         return lines, 1
    8.     return inspect.getblock(lines[lineno:]), lineno+1

    至此可知,如果ll命令无法获得源代码,单独通过inspect模块中的方法,也是无法获取的。

    n(next)命令

    1. def do_next(self, arg):
    2.     """n(ext)
    3.         Continue execution until the next line in the current function
    4.         is reached or it returns.
    5.         """
    6.     self.set_next(self.curframe)
    7.     return 1
    8. do_n = do_next
    9. def set_next(self, frame):
    10.     """Stop on the next line in or below the given frame."""
    11.     self._set_stopinfo(frame, None)

    其核心逻辑在_set_stopinfo方法中,该方法主要设置pdb调试过程中,需要中断在哪一帧这样的效果,代码如下:

    1. def _set_stopinfo(self, stopframe, returnframe, stoplineno=0):
    2.         """Set the attributes for stopping.
    3.         If stoplineno is greater than or equal to 0, then stop at line
    4.         greater than or equal to the stopline.  If stoplineno is -1, then
    5.         don't stop at all.
    6.         """
    7.         self.stopframe = stopframe
    8.         self.returnframe = returnframe
    9.         self.quitting = False
    10.         # stoplineno >= 0 means: stop at line >= the stoplineno
    11.         # stoplineno -1 means: don't stop at all
    12.         self.stoplineno = stoplineno

    代码并不复杂,就是设置不同变量的值而已,它之所以能实现中断效果,是该方法设置的值,会影响stop_here方法。

    前面提到了dispatch_#event相关的方法,都会调用stop_here方法判断这里是否需要中断,而stop_here方法会基于_set_stopinfo方法中的设置的变量来判断是否要中断。

    简单而言,当我们调用n命令时,会通过_set_stopinfo方法更新断点信息,让pdb在下一行也是中断的状态。

    s(step)命令

    1. def do_step(self, arg):
    2.         """s(tep)
    3.         Execute the current line, stop at the first possible occasion
    4.         (either in a function that is called or in the current
    5.         function).
    6.         """
    7.         self.set_step()
    8.         return 1
    9.     do_s = do_step
    10.     
    11.     def set_step(self):
    12.         """Stop after one line of code."""
    13.         # Issue #13183: pdb skips frames after hitting a breakpoint and running
    14.         # step commands.
    15.         # Restore the trace function in the caller (that may not have been set
    16.         # for performance reasons) when returning from the current frame.
    17.         if self.frame_returning:
    18.             caller_frame = self.frame_returning.f_back
    19.             if caller_frame and not caller_frame.f_trace:
    20.                 # 跟踪方法赋值给f_trace
    21.                 caller_frame.f_trace = self.trace_dispatch
    22.         self._set_stopinfo(None, None)

    比n命令多了设置caller_frame的逻辑,关键的一行在于caller_frame.f_trace = self.trace_dispatch,从而让pdb可以进入函数内部进行debug。

    u(up)命令

    u命令主要是回到上一栈帧,比如我们通过s命令,进入了函数体后,有些变量或方法无法使用了,我们可能会想回到上一栈帧,此时便可以通过u命令实现。

    u命令代码如下:

    1. def _select_frame(self, number):
    2.         assert 0 <= number < len(self.stack)
    3.         self.curindex = number
    4.         # 回到上一栈帧
    5.         self.curframe = self.stack[self.curindex][0]       
    6.         self.curframe_locals = self.curframe.f_locals
    7.         self.print_stack_entry(self.stack[self.curindex])
    8.         self.lineno = None
    9.     def do_up(self, arg):
    10.         """u(p) [count]
    11.         Move the current frame count (default one) levels up in the
    12.         stack trace (to an older frame).
    13.         """
    14.         if self.curindex == 0:
    15.             self.error('Oldest frame')
    16.             return
    17.         try:
    18.             count = int(arg or 1)
    19.         except ValueError:
    20.             self.error('Invalid frame count (%s)' % arg)
    21.             return
    22.         if count < 0:
    23.             newframe = 0
    24.         else:
    25.             newframe = max(0, self.curindex - count)
    26.         self._select_frame(newframe)
    27.     do_u = do_up

    从代码可知,u命令回到上一个栈帧的效果主要基于self.stack列表(调用栈)实现,而之所以能获得上一栈帧中的变量与方法,是因为栈帧对象中关联着当前栈帧的局部变量、全局变量等信息。

    结尾

    至此,pdb的源码就剖析完了,通过本文,你应该知道个大概了,但要深入其中,还是需要靠各位自己去调试与研究。

    最后,惯例卖卖自己的书,如果你或身边的朋友想学Python或Python自动化相关的内容,可以购买我的书籍,目前5折再售哟。

    13396d3cc24e78048bd4545403bab410.png

  • 相关阅读:
    手把手教你区块链java开发智能合约nft-第一篇
    Fiddler 手机抓包代理设置(针对华为荣耀60S)
    高新技术企业认定中涉及的领域有哪些?
    小数在计算机的存储形式
    java-php-net-python-基于的相册软件的设计与实现计算机毕业设计程序
    如何在idea中使用maven搭建tomcat环境
    Rpc-实现Client对ZooKeeper的服务监听
    Serialiable接口和serialVersionUID的作用及其在redisson中的应用
    ESP8266-Arduino编程实例-HP303B数字气压传感器驱动
    C# 日志框架Serilog使用
  • 原文地址:https://blog.csdn.net/weixin_30230009/article/details/125476790